Parámetro tipo covarianza en especializaciones.

tl; dr

¿Qué estrategias existen para superar la invariancia de tipo de parámetro para especializaciones, en un lenguaje (PHP) sin soporte para genéricos?

Nota: Me gustaría poder decir que mi comprensión de la teoría de tipos / seguridad / varianza / etc. fue más completa; No soy un CS importante.

Situación

Tienes una clase abstracta,Consumer, que te gustaría extender.Consumer declara un método abstractoconsume(Argument $argument) que necesita una definición. No debería ser un problema.

Problema

Tu especializadaConsumer, llamadoSpecializedConsumer no tiene negocios lógicos trabajando concada tipo deArgument. En su lugar, debería aceptar unSpecializedArgument (y sus subclases). Nuestro método de firma cambia aconsume(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.
    }
}

Estamos rompiendoPrincipio de sustitución Liskov, y causando problemas de seguridad de tipo. Mierda.

Pregunta

Ok, entonces esto no va a funcionar. Sin embargo, dada esta situación, ¿qué patrones o estrategias existen para superar el tipo de problema de seguridad y la violación deLSP, sin embargo, sigue manteniendo la relación de tipo deSpecializedConsumer aConsumer?

Supongo que es perfectamente aceptable que una respuesta pueda ser destilada hasta "ya dun goofed, de vuelta al tablero de dibujo".

Consideraciones, detalles y erratas

Muy bien, una solución inmediata se presenta como "no defina elconsume() método enConsumer". Ok, eso tiene sentido, porque la declaración del método es tan buena como la firma. Semánticamente a pesar de la ausencia deconsume()Incluso con una lista de parámetros desconocidos, me duele un poco el cerebro. Quizás haya una mejor manera.

Por lo que estoy leyendo, pocos idiomas admiten el tipo de parámetro covarianza; PHP es uno de ellos, y es el lenguaje de implementación aquí. Complicando más las cosas, he visto creativas ".soluciones"involucrandogenéricos; Otra característica no es compatible con PHP.

De WikiVarianza (ciencias de la computación) - ¿Necesidad de tipos de argumentos covariantes?:

Esto crea problemas en algunas situaciones, donde los tipos de argumentos deben ser covariantes para modelar los requisitos de la vida real. Supongamos que tienes una clase que representa a una persona. Una persona puede ver al médico, por lo que esta clase podría tener un método vacío virtualPerson::see(Doctor d). Ahora suponga que quiere hacer una subclase dePerson clase,Child. Eso es unChild es una persona. A uno le gustaría hacer una subclase deDoctor, Pediatrician. Si los niños solo visitan pediatras, nos gustaría imponer eso en el sistema de tipos. Sin embargo, una implementación ingenua falla: porque unaChild es unPerson, Child::see(d) debe tomar cualquierDoctor, no solo unPediatrician.

El artículo continúa diciendo:

En este caso, elpatrón de visitante podría ser utilizado para hacer cumplir esta relación. Otra forma de resolver los problemas, en C ++, es usandoprogramación genérica.

Otra vez,genéricos Puede ser utilizado creativamente para resolver el problema. Estoy explorando elpatrón de visitanteDe todos modos, como tengo una implementación a medias a medias, sin embargo, la mayoría de las implementaciones, como se describe en los artículos, aprovechan la sobrecarga de métodos, otra característica no compatible en PHP.

<too-much-information>

Implementación

Debido a la discusión reciente, ampliaré los detalles específicos de la implementación que no he incluido (como en, probablemente incluiré demasiado).

Por brevedad, he excluido cuerpos de métodos para aquellos que son (debiera ser) abundantemente claro en su propósito. Heintentó para ser breve, pero tiendo a decirlo. No quería volcar un muro de código, así que las explicaciones siguen / preceden a los bloques de código. Si tiene privilegios de edición y desea limpiar esto, hágalo. Además, los bloques de código no son copia-pasta de un proyecto. Si algo no tiene sentido, podría no serlo; grítame para aclarar.

Con respecto a la pregunta original, en lo sucesivoRule clase es laConsumer y elAdapter clase es laArgument.

Las clases relacionadas con el árbol se componen de la siguiente manera:

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
    }
}

Así que cadaInnerNode contieneRule yNode objetos, y cada unoOuterNode solamenteRule objetos.Node::evaluate() evalúa cadaRule (Node::evaluateEachRule()) a un booleanotrue. Si cada unoRule pasa, elNode ha pasado y esCommand se agrega a laWrapper, y descenderá a los niños para su evaluación (OuterNode::evaluateEachNode()), o simplemente volvertrue, paraInnerNode yOuterNode objetos respectivamente.

Como paraWrapper; laWrapper objeto proxies aRequest objeto, y tiene una colección deAdapter objetos. losRequest objeto es una representación de la solicitud HTTP. losAdapter objeto es una interfaz especializada (y mantiene un estado específico) para uso especifico con especificoRule objetos. (aquí es donde entran los problemas de LSP)

losCommand objeto es una acción (una devolución de llamada cuidadosamente empaquetado, realmente) que se añade a laWrapper objeto, una vez que todo está dicho y hecho, la matriz deCommand Los objetos serán disparados en secuencia, pasando elRequest (entre otras cosas) en.

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);
}

Así que un usuario dado de la tierraRule acepta la tierra de usuario esperadaAdapter. Si elAdapter necesita información sobre la solicitud, se enruta a través deWrapper, con el fin de preservar la integridad del originalRequest.

Como elWrapper agregadosAdapter objetos, pasará instancias existentes a posterioresRule objetos, para que el estado de unaAdapter se conserva de unoRule a la siguiente Una vezun entero el árbol ha pasado,Wrapper::commit() Se llama, y ​​cada uno de los agregados.Adapter Los objetos aplicarán su estado según sea necesario contra el original.Request.

Nos quedamos con una serie deCommand objetos, y una modificadaRequest.

¿Qué demonios es el punto?

Bueno, no quería recrear la "tabla de enrutamiento" prototípica común en muchos frameworks / aplicaciones de PHP, así que en lugar de eso fui con un "árbol de enrutamiento". Al permitir reglas arbitrarias, puede crear y agregar rápidamente unAuthRule (por ejemplo) a unNode, y ya no es toda la rama accesible sin pasar laAuthRule. En teoria (en mi cabeza) es como un unicornio mágico, que evita la duplicación de código y hace cumplir la organización de zona / módulo. En la práctica, estoy confundido y asustado.

¿Por qué dejé este muro de tonterías?

Bueno, esta es la implementación para la que necesito solucionar el problema LSP. CadaRule corresponde a unAdapter, y eso no es bueno. Quiero preservar la relación entre cada unoRule, para garantizar la seguridad de tipos al construir el árbol, etc., sin embargo, no puedo declarar el método clave (evaluate()) en el resumenRule, como la firma cambia para los subtipos.

En otra nota, estoy trabajando en la clasificación de laAdapter esquema de creación / gestión; si es responsabilidad de laRule para crearlo, etc.

</too-much-information>

Respuestas a la pregunta(2)

Su respuesta a la pregunta