Какие существуют стратегии преодоления инвариантности типа параметров для специализаций на языке ( 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 } }