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

ТЛ; др

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

Я полагаю, что вполне приемлемо, что ответ можно перефразировать так: "Ты, дун, тупой, обратно на чертежную доску ".


Соображения, детали и ошибки

  • Хорошо, немедленное решение представляется как " не определять consume() метод в Consumer ". Хорошо, это имеет смысл, потому что объявление метода только так хорошо, как подпись. Семантически, хотя отсутствие consume(), даже с неизвестным списком параметров, немного болит мой мозг. Возможно, есть лучший способ.

  • Из того, что я читаю, немногие языки поддерживают ковариацию типов параметров; PHP является одним из них и является языком реализации здесь. Еще больше усложняя вещи, я видел творческие " решения " с использованием дженериков; другая функция не поддерживается в PHP.

  • Из Wiki's Variance (информатика) - Нужны ковариантные типы аргументов?:

    Это создает проблемы в некоторых ситуациях, когда типы аргументов должны быть ковариантными для моделирования реальных требований. Предположим, у вас есть класс, представляющий человека. Человек может обратиться к врачу, поэтому этот класс может иметь метод виртуальной пустоты 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 объекты. 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 (например) к Node и эта ветвь больше не доступна без передачи AuthRule, Теоретически (в моей голове) это похоже на волшебного единорога, предотвращающего дублирование кода и обеспечивающего организацию зон / модулей. На практике я запутался и испугался.

Почему я покинул эту стену чепухи?

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

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

</too-much-information>

2 ответа

Решение

Чтобы правильно ответить на этот вопрос, мы должны сделать шаг назад и посмотреть на проблему, которую вы пытаетесь решить, более общим образом (и ваш вопрос уже был довольно общим).

Настоящая проблема

Настоящая проблема заключается в том, что вы пытаетесь использовать наследование для решения проблемы бизнес-логики. Это никогда не сработает из-за нарушений LSP и, что более важно, из-за жесткой связи вашей бизнес-логики со структурой приложения.

Таким образом, наследование является способом решения этой проблемы (по вышеизложенному и по причинам, которые вы указали в вопросе). К счастью, есть ряд композиционных паттернов, которые мы можем использовать.

Теперь, учитывая общий характер вашего вопроса, будет очень трудно найти надежное решение вашей проблемы. Итак, давайте рассмотрим несколько шаблонов и посмотрим, как они могут решить эту проблему.

стратегия

Образец стратегии - это первое, что пришло мне в голову, когда я впервые прочитал вопрос. По сути, он отделяет детали реализации от деталей исполнения. Это позволяет существовать нескольким различным "стратегиям", и вызывающая сторона должна определить, какую из них загрузить для конкретной проблемы.

Недостатком является то, что звонящий должен знать о стратегиях, чтобы выбрать правильную. Но это также позволяет провести четкое различие между различными стратегиями, так что это достойный выбор...

команда

Шаблон команд также отделил бы реализацию так же, как и стратегия. Основное отличие состоит в том, что в Стратегии звонящий - это тот, кто выбирает потребителя. В Команде это кто-то другой (возможно, фабрика или диспетчер)...

Каждый "Специализированный потребитель" будет реализовывать только логику для конкретного типа проблемы. Тогда кто-то другой сделает соответствующий выбор.

Сеть Ответственности

Следующим шаблоном, который может быть применим, является шаблон цепочки ответственности. Это похоже на шаблон стратегии, рассмотренный выше, за исключением того, что вместо того, чтобы решить, какой потребитель вызывается, каждая из стратегий вызывается последовательно, пока одна из них не обработает запрос. Итак, в вашем примере вы бы использовали более общий аргумент, но проверьте, является ли он конкретным. Если это так, обработайте запрос. В противном случае, пусть следующий попробует...

Мост

Паттерн Мост может быть уместным и здесь. Это в некотором смысле похоже на шаблон "Стратегия", но отличается тем, что реализация моста выберет стратегию во время построения, а не во время выполнения. Тогда для каждой реализации вы создадите своего "потребителя", детали которого будут скомпонованы как зависимости.

Шаблон посетителя

Вы упомянули Шаблон посетителя в своем вопросе, поэтому я решил, что упомяну об этом здесь. Я не совсем уверен, что это уместно в этом контексте, потому что посетитель действительно похож на шаблон стратегии, который предназначен для пересечения структуры. Если у вас нет структуры данных для обхода, то шаблон посетителя будет подвергнут дистилляции, чтобы выглядеть довольно похожим на шаблон стратегии. Я говорю честно, потому что направление контроля отличается, но конечные отношения в значительной степени одинаковы.

Другие паттерны

В конце концов, это действительно зависит от конкретной проблемы, которую вы пытаетесь решить. Если вы пытаетесь обрабатывать HTTP-запросы, где каждый "потребитель" обрабатывает свой тип запроса (XML против HTML против JSON и т. Д.), Лучший выбор, вероятно, будет сильно отличаться от того, что вы пытаетесь найти геометрическую область полигон Конечно, вы можете использовать один и тот же шаблон для обоих, но на самом деле это не одна и та же проблема.

При этом проблема также может быть решена с помощью шаблона посредника (в случае, когда нескольким "потребителям" требуется возможность обрабатывать данные), шаблона состояния (в случае, когда "потребитель" будет зависеть от прошлых использованных данных) или даже шаблон адаптера (в случае, когда вы абстрагируете другую подсистему в специализированном потребителе)...

Короче говоря, это сложная проблема, потому что есть так много решений, которые трудно сказать, что является правильным...

Единственная известная мне стратегия "сделай сам": прими простое Argument в определении функции и сразу проверьте, достаточно ли она специализирована:

class SpecializedConsumer extends Consumer {
    public function consume(Argument $argument) {
        if(!($argument instanceof SpecializedArgument)) {
            throw new InvalidArgumentException('Argument was not specialized.');
        }
        // move on
    }
}
Другие вопросы по тегам