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