У меня есть объекты и репозитории в моем проекте. Чтобы упростить, у меня есть
EntityInterface
UserEntity
BusinessEntity
Интерфейс:
interface Entity
{
/**
* @return EntityId
*/
public function getId();
}
Реализации
class UserEntity implements Entity
{
/**
* @return EntityId
*/
public function getId(){
//...do something here for return
return $userId;
}
}
а также
class BusinessEntity implements Entity
{
/**
* @return EntityId
*/
public function getId(){
//...do something here for return
return $userId;
}
}
Я хотел бы определить базовую функциональность репозитория, как save
, так что мой интерфейс выглядит так:
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);
}
Позже у меня есть разные интерфейсы для разных типов репозиториев, например UserRepository
а также 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);
}
Вышеприведенный код не работает, потому что Declaration must be compatible with Repository...
однако BusinessEntity реализует Entity, поэтому он совместим.
У меня много типов сущностей, поэтому, если я не могу напечатать подсказку, мне всегда нужно проверить, что переданный экземпляр является экземпляром того, что мне нужно. Это глупо.
Следующий код снова не работает:
class BusinessRepository implements Repository
{
public function save(BusinessEntity $entity)
{
//this will fail, however BusinessEntity is an Entity
}
}
В общем, параметры метода должны быть противоположными по отношению к иерархии наследования или инвариантными. Это означает, что BusinessEntity действительно не быть «совместимым» с Entity, когда используется как тип для параметра метода.
Подумайте об этом с точки зрения «контракта». Ваш интерфейс Repository
обещания что его метод save
может обрабатывать аргументы типа Entity
, Подтипы, наследуемые от Repository
должен быть привязан к этому введенному контракту (потому что в противном случае, какой смысл иметь определение типов в первую очередь, если вы не можете быть уверены, что они обещают сделать?).
Теперь, если подтип вдруг принимает только более специальные типы, такие как BusinessEntity
, но не больше Entity
, договор нарушен. Вы не можете использовать BusinessRepository
как Repository
больше, потому что вы не можете позвонить save
с Entity
,
Сначала это нелогично, но взгляните на это: https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)#Contravariant_method_argument_type
Обратите внимание на стрелку наследования на изображении.
Что делать? Избавьтесь от идеи наследования как святого Грааля в объектно-ориентированном программировании. В большинстве случаев это не так, и вводит все виды неприятных связей. Например, композицию, отдавшую предпочтение наследованию. Посмотри на Тип параметра ковариация в специализациях.
Это терпит неудачу, потому что вы объявляете методы, которые принимают разные аргументы в интерфейсах. Также возникает вопрос, существует ли какая-либо иная логика в сохранении BusinessEntity, чем Entity. Я думаю, что это не должно быть. Таким образом, вы можете опустить функцию сохранения в бизнес-объекте и сохранить только работу на Entity и должны знать, что Entity имеет метод «save».
Другой способ — использовать фабричный шаблон или абстрактную фабрику вместо наследования.