Intereting Posts
конвертировать mysql timestamp в фактическую дату и время? Лучшие практики в PHP и MySQL с международными строками Как отправить общую сумму, подлежащую выплате в PayPal Как отправить сообщение от WhatsApp в PHP с помощью WhatsAPI Official? Пытается генерировать пропорционально обрезанные миниатюры с фиксированной шириной / высотой с помощью PHP GD Google OAuth2 – isAccessTokenExpired () всегда верно Как перейти на якорь на submit – не вверху страницы Как вы создаете cakephp на общем хостинге? CakePHP 2.5.7 – Ошибка аутентификации при входе в систему с нестандартным пользователем Запустите PHP-код, когда пользователь нажимает ссылку и передает переменные Сортировать по полю в cakephp Отображение базы данных PHP в тех же самых полях с различными привязными тегами Случайный диапазон без дубликатов в PHP PHP взрывает массив, затем перебирает значения и выводит переменную Виды доступа в моделях / контроллерах

Ковариация типов параметров в специализациях

ТЛ; др

Какие существуют стратегии преодоления инвариантности типа параметров для специализаций на языке ( PHP ) без поддержки дженериков?

Примечание. Мне хотелось бы сказать, что мое понимание теории типов / безопасности / дисперсии / и т. Д. Было более полным; Я не главный.


ситуация

У вас есть абстрактный класс, Consumer , который вы хотели бы расширить. Consumer объявляет абстрактный метод consume(Argument $argument) который нуждается в определении. Не должно быть проблемой.


проблема

У вашего специализированного Consumer под названием SpecializedConsumer нет логического бизнеса, работающего с каждым типом Argument . Вместо этого он должен принять SpecializedArgument ( и их подклассы ). Наша подпись метода изменяется на consume(SpecializedArgument $argument) .

 abstract class Argument { } class SpecializedArgument extends Argument { } abstract class Consumer { abstract public function consume(Argument $argument); } class SpecializedConsumer extends Consumer { public function consume(SpecializedArgument $argument) { // i dun goofed. } } 

Мы нарушаем принцип замещения Лискова и вызываем проблемы безопасности типа. Полуют.


Вопрос

Хорошо, так что это не сработает. Однако, учитывая эту ситуацию, какие шаблоны или стратегии существуют для преодоления проблемы безопасности типа и нарушения LSP , но все же сохраняют отношение типа SpecializedConsumer к Consumer ?

Я полагаю, что вполне приемлемо, чтобы ответ можно было отвлечь на « ya dun goofed, обратно на чертежную доску ».


Соображения, детали и исправления

  • Хорошо, немедленное решение представляет собой « не определять метод consume() в Consumer ». Хорошо, это имеет смысл, потому что объявление метода так же хорошо, как и подпись. Семантически, хотя отсутствие consume() , даже с неизвестным списком параметров, немного повреждает мой мозг. Возможно, есть лучший способ.

  • Из того, что я читаю, несколько языков поддерживают ковариацию параметров параметров; PHP является одним из них и является языком реализации здесь. Дальнейшее усложнение вещей, я видел творческие « решения », связанные с дженериками ; другая функция не поддерживается в PHP.

  • Из разницы Wiki (компьютерная наука) – Необходимость в ковариантных типах аргументов? :

    Это создает проблемы в некоторых ситуациях, когда типы аргументов должны быть ковариантными для моделирования реальных требований. Предположим, у вас есть класс, представляющий человека. Человек может видеть врача, поэтому у этого класса может быть метод virtual void Person::see(Doctor d) . Теперь предположим, что вы хотите создать подкласс класса Person , Child . То есть, Child – это Человек. Тогда можно было бы сделать подкласс Doctor , Pediatrician . Если дети посещают только педиатров, мы хотели бы обеспечить их соблюдение в системе типов. Однако наивная реализация не выполняется: потому что Child – это Person , Child::see(d) должен принимать любого Doctor , а не только Pediatrician .

    Далее в статье говорится:

    В этом случае шаблон посетителя можно использовать для обеспечения соблюдения этого отношения. Другой способ решения проблем на C ++ – это использование общего программирования .

    Опять же, дженерики могут быть использованы творчески для решения проблемы. Я изучаю шаблон посетителя , так как в любом случае у меня есть его реализация, но большинство реализаций, описанных в статье, перегружают метод, и еще одна неподдерживаемая функция в PHP.


<too-much-information>

Реализация

Из-за недавнего обсуждения я расскажу о конкретных деталях реализации, которые я забыл включить ( например, я, вероятно, включу слишком много ).

Для краткости я исключил тела методов для тех, которые ( должны быть ) достаточно ясны в их назначении. Я пытался сохранить это краткое изложение, но я стараюсь быть многословным. Я не хотел сбрасывать стену кода, поэтому объяснения следуют за блоками кода. Если у вас есть права на редактирование и вы хотите очистить его, сделайте это. Кроме того, кодовые блоки не являются копиями макаронных изделий из проекта. Если что-то не имеет смысла, это может и не быть; кричать на меня для уточнения.

Что касается первоначального вопроса, в дальнейшем класс Rule является классом Consumer а Adapter является Argument .

Дерево связанные классы состоят из следующего:

 abstract class Rule { abstract public function evaluate(Adapter $adapter); abstract public function getAdapter(Wrapper $wrapper); } abstract class Node { protected $rules = []; protected $command; public function __construct(array $rules, $command) { $this->addEachRule($rules); } public function addRule(Rule $rule) { } public function addEachRule(array $rules) { } public function setCommand(Command $command) { } public function evaluateEachRule(Wrapper $wrapper) { // see below } abstract public function evaluate(Wrapper $wrapper); } class InnerNode extends Node { protected $nodes = []; public function __construct(array $rules, $command, array $nodes) { parent::__construct($rules, $command); $this->addEachNode($nodes); } public function addNode(Node $node) { } public function addEachNode(array $nodes) { } public function evaluateEachNode(Wrapper $wrapper) { // see below } public function evaluate(Wrapper $wrapper) { // see below } } class OuterNode extends Node { public function evaluate(Wrapper $wrapper) { // see below } } 

Таким образом, каждый InnerNode содержит объекты Rule и Node и каждый OuterNode Rule только для OuterNode . Node::evaluate() оценивает каждое Rule ( Node::evaluateEachRule() ) для логического значения true . Если каждое Rule проходит, Node пропускается, и его Command добавляется в Wrapper и спускается к детям для оценки ( OuterNode::evaluateEachNode() ) или просто возвращает true для InnerNode и OuterNode соответственно.

Что касается Wrapper ; объект Wrapper объект Request и имеет набор объектов Adapter . Объект Request является представлением HTTP-запроса. Объект Adapter является специализированным интерфейсом ( и поддерживает определенное состояние ) для конкретного использования с конкретными объектами Rule . ( здесь возникают проблемы LSP )

Объект Command является действием ( аккуратно упакованным обратным вызовом, действительно ), которое добавляется к объекту Wrapper , как только все будет сказано и сделано, массив объектов Command будет запущен последовательно, передав Request ( между прочим ) в.

 class Request { // all teh codez for HTTP stuffs } class Wrapper { protected $request; protected $commands = []; protected $adapters = []; public function __construct(Request $request) { $this->request = $request; } public function addCommand(Command $command) { } public function getEachCommand() { } public function adapt(Rule $rule) { $type = get_class($rule); return isset($this->adapters[$type]) ? $this->adapters[$type] : $this->adapters[$type] = $rule->getAdapter($this); } public function commit(){ foreach($this->adapters as $adapter) { $adapter->commit($this->request); } } } abstract class Adapter { protected $wrapper; public function __construct(Wrapper $wrapper) { $this->wrapper = $wrapper; } abstract public function commit(Request $request); } 

Таким образом, данное пользовательское Rule земли принимает ожидаемый Adapter пользовательских земель. Если Adapter нужна информация о запросе, он маршрутизируется через Wrapper , чтобы сохранить целостность исходного Request .

Когда Wrapper объединяет объекты Adapter , он передает существующие экземпляры в последующие объекты Rule , так что состояние Adapter сохраняется от одного Rule к другому. Когда все дерево прошло, вызывается Wrapper::commit() , и каждый из агрегированных объектов Adapter будет применять свое состояние по мере необходимости к исходному Request .

Затем мы оставляем массив объектов Command и модифицированный Request .


Что, черт возьми, смысл?

Ну, я не хотел воссоздавать прототипную «таблицу маршрутизации», общую во многих фреймворках / приложениях PHP, поэтому вместо этого я пошел с «деревом маршрутизации». AuthRule произвольные правила, вы можете быстро создать и добавить AuthRule ( например ) к Node , и больше не доступна вся ветка, не передавая AuthRule . Теоретически ( в моей голове ) это похоже на волшебный единорог, предотвращая дублирование кода и обеспечивая организацию зоны / модуля. На практике я в замешательстве и испуге.

Почему я оставил эту стену ерунды?

Ну, это реализация, для которой мне нужно исправить проблему LSP. Каждое Rule соответствует Adapter , и это плохо. Я хочу сохранить связь между каждым Rule , чтобы обеспечить безопасность типа при построении дерева и т. Д., Однако я не могу объявить ключевой метод ( evaluate() ) в абстрактном Rule , поскольку подписи изменяются для подтипов.

С другой стороны, я работаю над сортировкой схемы создания / управления Adapter ; независимо от того, является ли это обязанностью Rule для его создания и т. д.

</too-much-information>

Чтобы правильно ответить на этот вопрос, мы должны действительно сделать шаг назад и посмотреть на проблему, которую вы пытаетесь решить более общим образом (и ваш вопрос был уже довольно общим).

Реальная проблема

Реальная проблема заключается в том, что вы пытаетесь использовать наследование для решения проблемы бизнес-логики. Это никогда не сработает из-за нарушений LSP и, что еще более важно, связано с вашей бизнес-логикой с структурой приложения.

Таким образом, наследование отсутствует как метод решения этой проблемы (для вышеизложенного и причины, которые вы указали в вопросе). К счастью, существует ряд композиционных моделей, которые мы можем использовать.

Теперь, учитывая, насколько общий ваш вопрос, будет очень сложно определить надежное решение вашей проблемы. Итак, давайте рассмотрим несколько шаблонов и посмотрим, как они могут решить эту проблему.

стратегия

Шаблон стратегии – это первое, что пришло мне в голову, когда я впервые прочитал вопрос. В принципе, он отделяет детали реализации от деталей выполнения. Это позволяет использовать несколько разных «стратегий», и вызывающий абонент определит, какую нагрузку для конкретной проблемы.

Недостатком здесь является то, что вызывающий должен знать о стратегиях, чтобы выбрать правильный. Но это также позволяет более чистое различие между различными стратегиями, поэтому это достойный выбор …

команда

Шаблон Command также будет отделять реализацию так же, как и стратегия. Основное различие заключается в том, что в Стратегии абонент выбирает потребителя. В Command, это кто-то другой (возможно, фабрика или диспетчер) …

Каждый «Специализированный потребитель» будет реализовывать только логику для определенного типа проблем. Тогда кто-то другой сделает правильный выбор.

Цепочка ответственности

Следующим шаблоном, который может быть применим, является « Цепь ответственности» . Это похоже на шаблон стратегии, рассмотренный выше, за исключением того, что вместо решения потребителя, которое вызывается, каждая из стратегий вызывается последовательно, пока не обрабатывается запрос. Итак, в вашем примере вы бы взяли более общий аргумент, но проверьте, является ли он конкретным. Если это так, обработайте запрос. В противном случае пусть следующий попробует …

Мост

Мост шаблон может быть уместным и здесь. Это в некотором смысле похоже на шаблон стратегии, но отличается тем, что реализация моста будет выбирать стратегию во время построения, а не во время выполнения. Таким образом, вы бы построили другого «потребителя» для каждой реализации, а детали составлены внутри как зависимости.

Шаблон посетителя

Вы упомянули шаблон посетителя в своем вопросе, поэтому я бы подумал, что я бы сказал об этом здесь. Я не уверен, что это уместно в этом контексте, потому что посетитель действительно похож на шаблон стратегии, который предназначен для пересечения структуры. Если у вас нет структуры данных для перемещения, тогда шаблон посетителя будет удален, чтобы выглядеть довольно похоже на шаблон стратегии. Я говорю честно, потому что направление управления отличается, но конечные отношения почти одинаковы.

Другие шаблоны

В конце концов, это действительно зависит от конкретной проблемы, которую вы пытаетесь решить. Если вы пытаетесь обрабатывать HTTP-запросы, где каждый «Потребитель» обрабатывает другой тип запроса (XML против HTML против JSON и т. Д.), Лучший выбор, вероятно, будет очень отличаться, чем если вы пытаетесь обрабатывать поиск геометрической области многоугольник. Конечно, вы можете использовать один и тот же шаблон для обоих, но это не проблема.

С учетом сказанного проблема также может быть решена с помощью шаблона посредника (в случае, когда множественные «потребители» нуждаются в возможности обрабатывать данные), шаблон состояния (в случае, когда «потребитель» будет зависеть от прошлых потребляемых данных) или даже шаблон адаптера (в случае, если вы абстрагируете другую подсистему у специализированного потребителя) …

Короче говоря, это трудная проблема, потому что есть так много решений, что трудно сказать, что правильно …

Единственная известная мне стратегия DIY: принять простой Argument в определении функции и сразу же проверить, достаточно ли она специализирована:

 class SpecializedConsumer extends Consumer { public function consume(Argument $argument) { if(!($argument instanceof SpecializedArgument)) { throw new InvalidArgumentException('Argument was not specialized.'); } // move on } }