Тип параметра ковариация в специализациях

tl;dr

Какие существуют стратегии для преодоления инвариантности типов параметров для специализаций на языке (PHP) без поддержки дженериков?

Note: I wish I could say my understanding of type theory/safety/variance/etc., was more complete; I'm no CS major.

Situation

У вас есть абстрактный класс,Consumer, который вы хотели бы продлить.Consumer объявляет абстрактный методconsume(Argument $argument) который нуждается в определении. Не должно быть проблемой.

Problem

Ваш специализированныйConsumer, называетсяSpecializedConsumer не имеет никакого логического бизнеса, работая сevery типArgument, Вместо этого он должен принятьSpecializedArgument (and subclasses thereof). Подпись нашего метода меняется на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.
    }
}

Мы ломаемсяПринцип подстановки Лисковаи вызывает проблемы безопасности типа. Полуют.

Question

Итак, это не сработает. Однако, учитывая эту ситуацию, какие существуют схемы или стратегии для преодоления проблемы безопасности типов и нарушенияLSP, но все еще сохраняют тип отношенийSpecializedConsumer вConsumer?

Я полагаю, что вполне приемлемо, чтобы ответ можно было перевести вниз до & quot;ya dun goofed, back to the drawing board& Quot ;.

Considerations, Details, & Errata

Alright, an immediate solution presents itself as "don't define the consume() method in Consumer". Ok, that makes sense, because method declaration is only as good as the signature. Semantically though the absence of consume(), even with a unknown parameter list, hurts my brain a bit. Perhaps there is a better way.

From what I'm reading, few languages support parameter type covariance; PHP is one of them, and is the implementation language here. Further complicating things, I've seen creative "solutions" involving generics; another feature not supported in PHP.

From Wiki's Variance (computer science) - Need for covariant argument types?:

This creates problems in some situations, where argument types should be covariant to model real-life requirements. Suppose you have a class representing a person. A person can see the doctor, so this class might have a method virtual void Person::see(Doctor d). Now suppose you want to make a subclass of the Person class, Child. That is, a Child is a Person. One might then like to make a subclass of Doctor, Pediatrician. If children only visit pediatricians, we would like to enforce that in the type system. However, a naive implementation fails: because a Child is a Person, Child::see(d) must take any Doctor, not just a Pediatrician.

The article goes on to say:

In this case, the visitor pattern could be used to enforce this relationship. Another way to solve the problems, in C++, is using generic programming.

Again, generics can be used creatively to solve the problem. I'm exploring the visitor pattern, as I have a half-baked implementation of it anyway, however most implementations as described in articles leverage method overloading, yet another unsupported feature in PHP.

<too-much-information>

Implementation

В связи с недавним обсуждением я остановлюсь на конкретных деталях реализации, которые я забыл включить (as in, I'll probably include way too much).

For brevity, I've excluded method bodies for those which are (should be) abundantly clear in their purpose. I've tried to keep this brief, but I tend to get wordy. I didn't want to dump a wall of code, so explanations follow/precede code blocks. If you have edit privileges, and want to clean this up, please do. Also, code blocks aren't copy-pasta from a project. If something doesn't make sense, it might not; yell at me for clarification.

Что касается первоначального вопроса, далееRule&nbsp;класс этоConsumer&nbsp;иAdapter&nbsp;класс это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&nbsp;содержитRule&nbsp;а такжеNode&nbsp;объекты, и каждыйOuterNode&nbsp;толькоRule&nbsp;объекты.Node::evaluate()&nbsp;оценивает каждыйRule (Node::evaluateEachRule()) к логическомуtrue, Если каждыйRule&nbsp;проходит,Node&nbsp;прошло, и этоCommand&nbsp;добавляется вWrapperи сойдет к детям на оценку (OuterNode::evaluateEachNode()) или просто вернутьtrue, заInnerNode&nbsp;а такжеOuterNode&nbsp;объекты соответственно.

Что касаетсяWrapper;Wrapper&nbsp;объект проксиRequest&nbsp;объект, и имеет коллекциюAdapter&nbsp;объекты. Request&nbsp;Объект представляет собой представление HTTP-запроса. Adapter&nbsp;объект является специализированным интерфейсом (and maintains specific state) для конкретного использования с конкретнымиRule&nbsp;объекты. (this is where the LSP problems come in)

Command&nbsp;объект есть действие (a neatly packaged callback, really), который добавляется кWrapper&nbsp;объект, когда все сказано и сделано, массивCommand&nbsp;объекты будут запускаться последовательно, передаваяRequest (among other things) в.

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&nbsp;принимает ожидаемого пользователя землиAdapter, ЕслиAdapter&nbsp;нужна информация о запросе, он направляется черезWrapper, чтобы сохранить целостность оригиналаRequest.

КакWrapper&nbsp;сводные показателиAdapter&nbsp;объекты, он будет передавать существующие экземпляры к последующимRule&nbsp;объекты, так что состояниеAdapter&nbsp;сохраняется от одногоRule&nbsp;к следующему. однаждыan entire&nbsp;дерево прошло,Wrapper::commit()&nbsp;называется, и каждый из агрегированныхAdapter&nbsp;объекты будут применять его состояние по мере необходимости к оригиналуRequest.

Затем мы остаемся с массивомCommand&nbsp;объекты и модифицированныйRequest.

What the hell is the point?

Ну, я не хотел воссоздавать прототип "таблицы маршрутизации" часто встречается во многих PHP-фреймворках / приложениях, поэтому вместо этого я использовал «дерево маршрутизации». Допуская произвольные правила, вы можете быстро создавать и добавлятьAuthRule (for example) кNodeи эта ветвь больше не доступна без передачиAuthRule, Теоретически (in my head) это похоже на волшебного единорога, предотвращающего дублирование кода и обеспечивающего организацию зон / модулей. На практике я смущен и напуган.

Why I left this wall of nonsense?

Ну, это реализация, для которой мне нужно исправить проблему LSP. каждыйRule&nbsp;соответствуетAdapterи это не хорошо. Я хочу сохранить отношения между каждымRuleдля обеспечения безопасности типов при построении дерева и т. д., однако я не могу объявить метод key (evaluate()) в аннотацииRule, так как подпись меняется для подтипов.

С другой стороны, я работаю надAdapter&nbsp;схема создания / управления; является ли это обязанностьюRule&nbsp;создать его и т. д.

</too-much-information>