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>