3

I have Entities and Repositories in my project. To simplify, I have

  • EntityInterface
  • UserEntity
  • BusinessEntity

Interface:

interface Entity
{
    /**
     * @return EntityId
     */
    public function getId();
}

Implementations

class UserEntity implements Entity
{
    /**
     * @return EntityId
     */
    public function getId(){
      //...do something here for return
      return $userId;
    }
}

and

class BusinessEntity implements Entity
{
    /**
     * @return EntityId
     */
    public function getId(){
      //...do something here for return
      return $userId;
    }
}

I would like to define a Repository base-functionality, like save, so my interface looks like:

interface Repository
{
    /**
     * @param Entity $entity
     *
     * @throws \InvalidArgumentException If argument is not match for the repository.
     * @throws UnableToSaveException If repository can't save the Entity.
     *
     * @return Entity The saved entity
     */
    public function save(Entity $entity);
}

Later, I have different interfaces for different type of Repositories, like UserRepository and BusinessRepository

interface BusinessRepository extends Repository
{
    /**
     * @param BusinessEntity $entity
     *
     * @throws \InvalidArgumentException If argument is not match for the repository.
     * @throws UnableToSaveException If repository can't save the Entity.
     *
     * @return Entity The saved entity
     */
    public function save(BusinessEntity $entity);
}

The above code fails, because Declaration must be compatible with Repository...

however BusinessEntity implements Entity, so it's compatible.

I have many type of entities, so If I can't type-hint, I always need to check, that the passed instance is instanceof what I need. It's stupid.

The following code fails again:

class BusinessRepository implements Repository
{
    public function save(BusinessEntity $entity)
    {
      //this will fail, however BusinessEntity is an Entity
    }
}
Rasclatt
  • 12,498
  • 3
  • 25
  • 33
fureszpeter
  • 120
  • 6

2 Answers2

1

In general, method parameters have to be contravariant with respect to an inheritance hierarchy or invariant. This means that indeed BusinessEntity would not be "compatible" with Entity when used as a type for a method parameter.

Think of it from a "contract" point of view. Your interface Repository promises that its method save can handle arguments of type Entity. Subtypes inheriting from Repository should be bound to this introduced contract (because otherwise, what sense would it make to define types in the first place, if you cannot be sure what they promises to be able to do?).

Now, if a subtype all of a sudden only accepts more special types, like BusinessEntity, but no longer Entity, the contract's broken. You cannot use BusinessRepository as Repository any more, because you cannot call save with an Entity.

This is counterintuitive at first, but have a look at this: https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)#Contravariant_method_argument_type

Notice the inheritance arrow in the image.

What's to do? Get rid of the idea of inheritance being the holy grail in object oriented programming. Most of the time, it is not, and introduces all kinds of nasty coupling. Favor composition over inheritance, for example. Have a look at Parameter type covariance in specializations.

Community
  • 1
  • 1
stef77
  • 1,000
  • 5
  • 19
0

It fails because you declare methods that takes different arguments in interfaces. There is also question if there is any different logic in saving BusinessEntity than Entity. I think it shouldn't be. So you can omit save function in business entity and save just work on Entity and should know that Entity has "save" method.

The other way is to use factory pattern or abstract factory over inheritance.

Robert
  • 19,800
  • 5
  • 55
  • 85
  • Still confused, because in SOLID, Liskov substitution principle says >if S is a subtype of T, then objects of type T may be replaced with objects of type S this is exactly the same what I want to do here. BusinessEntity is an Entity, implements Entity, I expect an Entity, and if BusinessEntity implements Entity, why I can't use BusinessEntity as an Entity... – fureszpeter Aug 29 '15 at 23:11