У меня есть некоторые классы обработчиков («контроллер»), и они могут обрабатывать элементы некоторым образом:
interface IHandler
{
public function execute(Item $item);
}
class FirstHandler implements IHandler
{
public function execute(Item $item) { echo $item->getTitle(); }
}
class SecondHandler implements IHandler
{
public function execute(Item $item) { echo $item->getId() . $item->getTitle(); }
}
class Item
{
public function getId() { return rand(); }
public function getTitle() { return 'title at ' . time(); }
}
Но тогда мне нужно добавить новую функциональность в дочерний класс Item:
class NewItem extends Item
{
public function getAuthor() { return 'author ' . rand(); }
}
и использовать его в SecondHandler
class SecondHandler implements IHandler
{
public function execute(Item $item) { printf('%d %s, author %s', $item->getId(), $item->getTitle(), $item->getAuthor()); }
}
Но Item
класс на самом деле не имеет getAuthor
метод. И, если я пытаюсь изменить сигнатуру метода accept в SecondHandler
класс, поймаю E_STRICT
ошибка о декларации совместимости. И, конечно, это своего рода нарушение LSP.
Как я могу исправить эту проблему? Нужно ли два интерфейса, например, INewHandler
а также IHandler
с разными подписями execute
метод? Но это своего рода дубликаты кода.
Кроме того, я не могу использовать __constructor(Item $item)
а также __construct(NewItem $item)
в обработчиках (и execute
метод без аргументов), что будет рассматриваться как лучшее решение: они должны быть неизменяемыми и иметь только один экземпляр каждой стратегии, допустимый в жизненном цикле приложения.
Как вы сами обнаружили, реализация PHP с хинтингом типов имеет много ограничений, которые делают сценарии, подобные описанному вами, сложнее, чем они должны быть. В других типизированных языках, таких как Java и Swift, ваша реализация абсолютно законна.
Подумав над вопросом, я пришел к решению, представленному Феликс но я считаю это слишком сложным по сравнению с проблемой.
Мой ответ на ваш вопрос — не решение, а совет, который я дам вам после нескольких лет разработки с PHP:
Откажитесь от подсказок типа в PHP и развивайтесь так, как должно быть … динамично.
PHP больше похож на Ruby / Python / JavaScript, чем Java / C ++, и попытка скопировать 1: 1 из статически типизированных языков приводит к принудительной и сложной реализации.
Решение вашей проблемы реализации легко, поэтому не усложняйте ее и не делайте так, как следует (принцип KISS).
Объявите аргументы методов без типа и реализуйте проверку там, где вам действительно нужно (например, сгенерировать исключение).
interface IStrategy
{
public function execute($item);
}
class FirstStrategy implements IStrategy
{
public function execute($item) {
echo $item->getTitle();
}
}
class SecondStrategy implements IStrategy
{
public function execute($item) {
// execute(NewItem $item) is identical to this check.
if (! $item instanceof NewItem) {
throw new Exception('$item must be an instance of NewItem');
}
echo $item->getAuthor();
}
}
class Item
{
public function getId() { return rand(); }
public function getTitle() { return 'title at ' . time(); }
}
class NewItem extends Item
{
public function getAuthor() { return 'author ' . rand(); }
}
Когда это возможно, старайтесь не строго определять тип параметров, а адаптировать поведение кода на основе доступных интерфейсов (Duck Typing).
class SecondStrategy implements IStrategy
{
public function execute($item) {
$message = $item->getTitle();
// PHP 5 interface availability check.
if (is_callable([$item, 'getAuthor'])) {
$message .= ' ' . $item->getAuthor();
}
// With PHP 7 is even better.
// try {
// $message .= ' ' . $item->getAuthor();
// } catch (Error $e) {}
echo $message;
}
}
Я надеюсь, что помог вам. ^ _ ^
Оба @ daniele-orlando и @ ihor-burlachenko сделали правильные замечания.
Рассмотрим следующий подход для перегрузки методов, который является своего рода компромиссом и должен хорошо масштабироваться:
interface IHandler
{
/**
* @param $item Item|NewItem
*/
public function execute($item);
// protected function executeItem(Item $item);
// protected function executeNewItem(NewItem $item);
}
trait IHandlerTrait
{
public function execute($item)
{
switch(true) {
case $item instanceof Item:
return $this->executeItem($item);
case $item instanceof NewItem:
return $this->executeNewItem($item);
default:
throw new \InvalidArgumentException("Unsupported parameter type " . get_class($item));
}
}
protected function executeItem(Item $item)
{
throw new \LogicException(__CLASS__ . " cannot handle execute() for type Item");
}
protected function executeNewItem(NewItem $item)
{
throw new \LogicException(__CLASS__ . " cannot handle execute() for type NewItem");
}
}
class FirstHandler implements IHandler
{
use IIHandlerTrait;
protected function executeItem(Item $item) { echo $item->getTitle(); }
}
class SecondHandler implements IHandler
{
use IIHandlerTrait;
// only if SecondHandler still need to support `Item` for backward compatibility
protected function executeItem(Item $item) { echo $item->getId() . $item-> getTitle(); }
protected function executeNewItem(NewItem $item) { printf('%d %s, author %s', $item->getId(), $item->getTitle(), $item->getAuthor()); }
}
Вы уверены, что хотите использовать шаблон стратегии здесь?
Похоже, что действие стратегии здесь зависит от типа элемента, который она обрабатывает. А также посетитель шаблон может применяться и здесь, в этом случае.
В существующем состоянии вы, похоже, хотите выполнить расширяемую запись данных (Item и NewItem). Рассмотрим вместо проведение некоторые подключаемые поведение (реализовано через интерфейс).
Из твоих писаний трудно догадаться, каково будет это поведение, потому что (Новый) Предмет — это просто прославленная структура данных в приведенном тобой примере.
Если вы хотите работать / манипулировать объектом в другом объекте, вы можете / должны использовать интерфейсы.
interface IStrategy
{
public function execute(ItemInterface $item);
}
interface ItemInterface
{
public function getTitle();
.....
}
Если вы хотите расширить публичную функциональность класса (New) Item, вы можете создать новый интерфейс для newItem
interface NewItemInterface extends ItemInterface
{
...
}
class SecondStrategy implements IStrategy
{
public function execute(NewItemInterface $item)
{ .... }
}
Или вы можете использовать некоторые проверки экземпляров, как уже упоминали другие.
Если ваше наследование и предположение, что SecondHandler должен обрабатывать как Item, так и NewItem, были правильными в первую очередь, то вы должны иметь возможность скрыть эту функциональность за общим интерфейсом. Из ваших примеров он может быть вызван toString (), который может быть частью интерфейса Item.
В противном случае, возможно, что-то не так с вашим дизайном изначально. И вы должны изменить свое наследство или способ обработки предметов. Или что-то еще, о чем мы не знаем.
Кроме того, я не знаю, зачем вам нужен DTO, но, похоже, есть некоторое недопонимание доктрины. Доктрина — это ORM, и она решает вашу проблему с постоянством. Он добавляет ограничения на то, как вы взаимодействуете со своим хранилищем, представляя репозитории, но не определяет логику вашего домена.
В соответствии с разделением интерфейса, пожалуйста, найдите какое-то решение.
« `
# based on interface segrigation.
interface BasicInfo
{
public function getId();
public function getTitle();
}
interface AuthorInfo
{
public function getAuthor();
}
interface IHandler
{
public function execute(Item $item);
}
class FirstHandler implements IHandler
{
public function execute(Item $item) { echo $item->getTitle(); }
}
class SecondHandler implements IHandler
{
public function execute(Item $item) { echo $item->getId() . $item->getTitle(); }
}
class Item implements BasicInfo
{
public function getId() { return rand(); }
public function getTitle() { return 'title at ' . time(); }
}
class Item2 extends Item implements AuthorInfo
{
public function getAuthor() { return 'author ' . rand(); }
}
Но я думаю, что вы не должны сохранять зависимость класса Item. Вы должны написать некоторый дублированный код, чтобы класс оставался подключаемым / независимым. Так что принцип Open / Close также должен быть там.