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>