Ковариация типов параметров в специализациях
TL;DR
Существуют ли стратегии преодоления инвариантности типа параметра для специализаций на языке (PHP) без поддержки дженериков?
Примечание. Мне хотелось бы сказать, что мое понимание теории типов/безопасности/дисперсии/и т.д. было более полным; Я не майор CS.
Ситуация
У вас есть абстрактный класс 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 является одним из них и является языком реализации здесь. Дальнейшее усложнение вещей, я видел творческие "решения" с участием generics; другая функция не поддерживается в PHP.
-
Из Wiki Разница (информатика) - Необходимость в ковариантных типах аргументов?:
Это создает проблемы в некоторых ситуациях, где типы аргументов должны быть ковариантными для моделирования реальных требований. Предположим, у вас есть класс, представляющий человека. Человек может видеть врача, поэтому у этого класса может быть метод virtual void Person::see(Doctor d)
. Теперь предположим, что вы хотите создать подкласс класса Person
, Child
. То есть, Child
- это Личность. Тогда можно было бы сделать подкласс Doctor
, Pediatrician
. Если дети посещают только педиатров, мы хотели бы обеспечить их соблюдение в системе типов. Однако наивная реализация не выполняется: поскольку Child
является Person
, Child::see(d)
должен принимать любые Doctor
, а не только Pediatrician
.
Далее в статье говорится:
В этом случае шаблон можно использовать для обеспечения соблюдения этого отношения. Другой способ решить проблемы на С++ - это общее программирование.
Опять же, generics можно творчески использовать для решения проблемы. Я изучаю шаблон посетителя, так как у меня есть его полузасужденная реализация в любом случае, однако большинство реализаций, описанных в перегрузке метода статей, еще одна неподдерживаемая функция в 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
объектов. Node::evaluate()
оценивает каждый Rule
(Node::evaluateEachRule()
) на boolean 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
(например) к Node
, и больше не доступна вся эта ветка без передачи AuthRule
. Теоретически (в моей голове) он похож на волшебного единорога, предотвращая дублирование кода и обеспечивая организацию зоны/модуля. На практике я запутался и испугался.
Почему я оставил эту стену ерунды?
Ну, это реализация, для которой мне нужно исправить проблему LSP. Каждой Rule
соответствует Adapter
, и это не хорошо. Я хочу сохранить взаимосвязь между каждым Rule
, чтобы обеспечить безопасность типа при построении дерева и т.д., Однако я не могу объявить ключевой метод (evaluate()
) в абстрактном Rule
, поскольку изменения подписи для подтипов.
В другой заметке я работаю над сортировкой схемы создания/управления Adapter
; отвечает ли Rule
за его создание и т.д.
</too-much-information>
Ответы
Ответ 1
Чтобы правильно ответить на этот вопрос, мы должны действительно сделать шаг назад и рассмотреть проблему, которую вы пытаетесь решить более общим образом (и ваш вопрос был уже довольно общим).
Реальная задача
Реальная проблема заключается в том, что вы пытаетесь использовать наследование для решения проблемы бизнес-логики. Это никогда не сработает из-за нарушений LSP и, что еще более важно, связано с вашей бизнес-логикой с структурой приложения.
Таким образом, наследование отсутствует как метод решения этой проблемы (для вышеизложенного и причины, которые вы указали в вопросе). К счастью, существует ряд композиционных шаблонов, которые мы можем использовать.
Теперь, учитывая, насколько общий ваш вопрос, будет очень сложно определить надежное решение вашей проблемы. Поэтому давайте рассмотрим несколько шаблонов и посмотрим, как они могут решить эту проблему.
Стратегия
Шаблон стратегии - это первое, что мне пришло в голову, когда я впервые прочитал вопрос. В принципе, он отделяет детали реализации от деталей выполнения. Это позволяет использовать несколько разных "стратегий", и вызывающий абонент определит, какую нагрузку для конкретной проблемы.
Недостатком здесь является то, что вызывающий должен знать о стратегиях, чтобы выбрать правильный. Но это также позволяет более чистое различие между различными стратегиями, поэтому это достойный выбор...
Command
Command Pattern также будет отделять реализацию так же, как и стратегия. Основное различие заключается в том, что в Стратегии абонент выбирает потребителя. В Command, это кто-то еще (factory или диспетчер, возможно)...
Каждый "Специализированный потребитель" будет реализовывать только логику для определенного типа проблем. Тогда кто-то другой сделает подходящий выбор.
Цепочка ответственности
Следующим шаблоном, который может быть применим, является Цепь ответственности. Это похоже на шаблон стратегии, рассмотренный выше, за исключением того, что вместо решения потребителя, которое вызывается, каждая из стратегий вызывается последовательно, пока не обрабатывается запрос. Итак, в вашем примере вы бы взяли более общий аргумент, но проверьте, является ли он конкретным. Если это так, обработайте запрос. В противном случае пусть следующий попробует...
Мост
A Мост шаблон может быть уместным здесь. Это в некотором смысле похоже на шаблон стратегии, но отличается тем, что реализация моста будет выбирать стратегию во время построения, а не во время выполнения. Таким образом, вы бы построили другого "потребителя" для каждой реализации, а детали составлены внутри как зависимости.
Шаблон посетителя
Вы упомянули шаблон посетителя в своем вопросе, поэтому я хотел бы упомянуть об этом здесь. Я не уверен, что это уместно в этом контексте, потому что посетитель действительно похож на шаблон стратегии, который предназначен для перемещения по структуре. Если у вас нет структуры данных для перемещения, тогда шаблон посетителя будет удален, чтобы выглядеть довольно похоже на шаблон стратегии. Я говорю справедливо, потому что направление управления отличается, но конечные отношения почти одинаковы.
Другие шаблоны
В конце концов, это действительно зависит от конкретной проблемы, которую вы пытаетесь решить. Если вы пытаетесь обрабатывать HTTP-запросы, где каждый "Потребитель" обрабатывает другой тип запроса (XML против HTML против JSON и т.д.), Лучший выбор, вероятно, будет очень отличаться, чем если вы пытаетесь обрабатывать поиск геометрической области многоугольник. Конечно, вы можете использовать один и тот же шаблон для обоих, но это не проблема.
С учетом сказанного проблема также может быть решена с помощью Посредника шаблона (в случае, когда несколько "Потребителей" нуждаются в возможности обрабатывать данные), a Шаблон состояния (в случае, когда "Потребитель" будет зависеть от прошлых потребляемых данных) или даже Шаблон адаптера (в случае, когда вы абстрагируете другую подсистему у специализированного потребителя)...
Короче говоря, это трудная проблема, потому что есть так много решений, что трудно сказать, что правильно...
Ответ 2
Единственная известная мне стратегия DIY: принять простой Argument
в определении функции и сразу проверить, достаточно ли она достаточно специализирована:
class SpecializedConsumer extends Consumer {
public function consume(Argument $argument) {
if(!($argument instanceof SpecializedArgument)) {
throw new InvalidArgumentException('Argument was not specialized.');
}
// move on
}
}