Design leading to break contravariance. How to avoid it?
https://softwareengineering.stackexchange.com/questions/419163
-
18-03-2021 - |
Question
I have a Converter
which use a Resolver
to determine which Factory
to use when converting a Resource
to a Entity
. To do so, I need to be sure that the Factory
has a createFromResource
method.
The problem: I am not able to create an interface handling the Factories because of the contravariance of arguments. The FactoryInterface
cannot set the argument of createFromResource
method because it will change in every Factory
implementation and doesn't have a common parent.
How can I design those classes without breaking Liskov Substitution Principle? What I am doing wrong? Is that a bad design?
Here are my classes:
interface EntityInterface {}
class UserEntity implements EntityInterface {}
interface ResourceInterface {}
class UserResource implements ResourceInterface {}
class Converter
{
private FactoryResolver $factoryResolver;
public function convert(ResourceInterface $resource): EntityInterface
{
$factory = $this->factoryResolver->resolveFromResource($resource);
// Need to ensure that $factory has createFromResource() method
return $factory->createFromResource($resource);
}
}
class FactoryResolver
{
public function resolveFromResource(ResourceInterface $resource): FactoryInterface;
}
interface FactoryInterface
{
// Cannot be implemented because of contravariance
public function createFromResource(ResourceInterface $resource): EntityInterface;
}
class UserFactory implements FactoryInterface
{
// This signature break contravariance
public function createFromResource(UserResource $user): UserEntity;
}
PHP version: 7.4
Thanks a lot !
Solution
I think your error highlights an important point: Is there even a situation where you could treat UserFactory
as if it was a FactoryInterface
?
Since UserFactory.createFromResource
requires a UserResource
, it's less permissive than FactoryInterface.createFromResource
. This means that casting UserResource
to FactoryInterface
is unsafe no matter how you look at it.
Covariance and contravariance only tell you what's guaranteed to be safe with respect to automatic type conversions. When implementing an inherited function, argument types can never be more restrictive than what the parent allows; otherwise, it would be unsafe to actually use it as an instance of the parent.
In summary, it appears that FactoryInterface
doesn't express a common property of its children in this case, since you can't discard knowledge of the underlying implementation.