type parameter hinting php types covariance

parameter - return type php



Covarianza de tipo de parĂ¡metro en especializaciones (2)

El único que conozco es la estrategia de bricolaje: acepte un Argument simple en la definición de la función y compruebe inmediatamente si es lo suficientemente especializado:

class SpecializedConsumer extends Consumer { public function consume(Argument $argument) { if(!($argument instanceof SpecializedArgument)) { throw new InvalidArgumentException(''Argument was not specialized.''); } // move on } }

tl; dr

¿Qué estrategias existen para superar la invarianza del 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 comandante de CS.

Situación

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

Problema

Su Consumer especializado, llamado SpecializedConsumer no tiene un negocio lógico que trabaje con cada tipo de Argument . En su lugar, debe aceptar un SpecializedArgument ( y subclases del mismo ). Nuestra firma de método cambia para 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. } }

Estamos rompiendo el principio de sustitución de Liskov y estamos causando problemas de seguridad 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 de LSP , y aún así mantener la relación tipo de SpecializedConsumer Consumer to Consumer ?

Supongo que es perfectamente aceptable que una respuesta se pueda resumir en " ya sabes, de vuelta al tablero de dibujo ".

Consideraciones, detalles y erratas

  • De acuerdo, una solución inmediata se presenta como " no definir el método de consume() en el Consumer ". Ok, eso tiene sentido, porque la declaración del método es tan buena como la firma. Semánticamente, aunque la ausencia de consume() , incluso con una lista de parámetros desconocidos, me duele un poco el cerebro. Tal vez hay una mejor manera.

  • Según lo que estoy leyendo, algunos idiomas admiten la covarianza del tipo de parámetro; PHP es uno de ellos, y es el lenguaje de implementación aquí. Para complicar aún más las cosas, he visto " soluciones " creativas relacionadas con los genéricos ; otra característica no admitida en PHP.

  • De la varianza de Wiki (ciencia 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 tiene 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 Virtual Void Person::see(Doctor d) . Ahora supongamos que quiere hacer una subclase de la clase Person , Child . Es decir, un Child es una persona. Entonces, uno podría querer hacer una subclase de Doctor , Pediatrician . Si los niños solo visitan pediatras, nos gustaría aplicar eso en el sistema de tipos. Sin embargo, una implementación ingenua falla: porque un Child es una Person , Child::see(d) debe llevar a cualquier Doctor , no solo a un Pediatrician .

    El artículo continúa diciendo:

    En este caso, el patrón de visitante podría usarse para hacer cumplir esta relación. Otra forma de resolver los problemas, en C ++, es usar programación genérica .

    Una vez más, los genéricos pueden usarse creativamente para resolver el problema. Estoy explorando el patrón de visitantes , ya que tengo una implementación a medias de todos modos, sin embargo, la mayoría de las implementaciones descritas en los artículos aprovechan la sobrecarga de métodos, otra característica no soportada en PHP.

<too-much-information>

Implementación

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

Para abreviar, he excluido cuerpos de métodos para aquellos que son ( deberían ser ) abundantemente claros en su propósito. Intenté mantener esto breve, pero tiendo a ser prolijo. No quería volcar una pared de código, así que las explicaciones siguen / preceden a los bloques de código. Si tienes privilegios de edición y quieres limpiar esto, hazlo. Además, los bloques de código no son copy-pasta de un proyecto. Si algo no tiene sentido, puede que no; grítame por aclaración.

Con respecto a la pregunta original, en lo sucesivo, la clase Rule es el Consumer y la clase Adapter es el Argument .

Las clases relacionadas con los árboles 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 } }

De modo que cada InnerNode contiene objetos Rule y Node , y cada Rule OuterNode solo se opone. Node::evaluate() evalúa cada Rule ( Node::evaluateEachRule() ) a un valor booleano true . Si cada Rule pasa, el Node ha pasado y su Command se agrega a la Wrapper , y descenderá a los niños para su evaluación ( OuterNode::evaluateEachNode() ), o simplemente devolverá true , para los objetos InnerNode y OuterNode , respectivamente.

En cuanto a Wrapper ; el objeto Wrapper envía un proxies a un objeto Request y tiene una colección de objetos Adapter . El objeto Request es una representación de la solicitud HTTP. El objeto Adapter es una interfaz especializada ( y mantiene un estado específico ) para uso específico con objetos Rule específicos. ( Aquí es donde entran los problemas LSP )

El objeto Command es una acción ( una devolución de llamada perfectamente empaquetada, en realidad ) que se agrega al objeto Wrapper , una vez que todo está dicho y hecho, la matriz de objetos Command se disparará en secuencia, pasando la Request ( 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); }

Por lo tanto, una Rule usuario-tierra determinada acepta el Adapter usuario-tierra esperado. Si el Adapter necesita información sobre la solicitud, se enruta a través de Wrapper , a fin de preservar la integridad de la Request original.

A medida que el Wrapper agrega objetos del Adapter , pasará las instancias existentes a los objetos de Rule subsiguientes, de modo que el estado de un Adapter se preserve de una Rule a la siguiente. Una vez que ha pasado todo un árbol, se llama a Wrapper::commit() , y cada uno de los objetos agregados del Adapter aplicará su estado según sea necesario contra la Request original.

Luego nos queda una matriz de objetos Command y una Request modificada.

¿Qué demonios es el punto?

Bueno, no quería volver a crear la "tabla de enrutamiento" prototípica común en muchos frameworks / aplicaciones PHP, así que en su lugar fui con un "árbol de enrutamiento". Al permitir reglas arbitrarias, puede crear y agregar rápidamente un AuthRule ( por ejemplo ) a un Node , y ya no se puede acceder a toda la rama sin pasar el AuthRule . En teoría ( en mi cabeza ) es como un unicornio mágico, lo que impide la duplicación de código y la aplicación de 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. Cada Rule corresponde a un Adapter , y eso no es bueno. Deseo preservar la relación entre cada Rule , para garantizar la seguridad de tipo al construir el árbol, etc., sin embargo, no puedo declarar el método clave ( evaluate() ) en la Rule abstracta, ya que la firma cambia para subtipos.

En otra nota, estoy trabajando en clasificar el esquema de creación / gestión del Adapter ; si es responsabilidad de la Rule crearlo, etc.

</too-much-information>


Para responder correctamente a esta pregunta, realmente debemos dar un paso atrás y analizar el problema que está tratando de resolver de una manera más general (y su pregunta ya era bastante general).

El problema real

El verdadero problema es que estás tratando de usar la herencia para resolver un problema de lógica comercial. Eso nunca va a funcionar debido a las violaciones de LSP y, lo que es más importante, a ajustar la lógica de su negocio a la estructura de la aplicación.

Así que la herencia está fuera como un método para resolver este problema (por lo anterior, y las razones que usted indicó en la pregunta). Afortunadamente, hay una serie de patrones de composición que podemos usar.

Ahora, teniendo en cuenta cuán genérica es su pregunta, será muy difícil identificar una solución sólida para su problema. Repasemos algunos patrones y veamos cómo pueden resolver este problema.

Estrategia

El patrón de estrategia es el primero que se me vino a la mente cuando leí la pregunta por primera vez. Básicamente, separa los detalles de implementación de los detalles de ejecución. Permite que existan varias "estrategias" diferentes, y quien llama determinaría qué carga para el problema en particular.

La desventaja aquí es que la persona que llama debe conocer las estrategias para elegir la correcta. Pero también permite una distinción más clara entre las diferentes estrategias, por lo que es una elección decente ...

Mando

El patrón de comando también desacoplaría la implementación al igual que lo haría Strategy. La principal diferencia es que en Estrategia, el que llama es el que elige al consumidor. En Comando, es otra persona (una fábrica o despachador tal vez) ...

Cada "Consumidor especializado" implementaría solo la lógica para un tipo específico de problema. Entonces alguien más tomaría la decisión adecuada.

Cadena de responsabilidad

El siguiente patrón que puede ser aplicable es el patrón de cadena de responsabilidad . Esto es similar al patrón de estrategia discutido anteriormente, excepto que en lugar de que el consumidor decida a cuál se llama, cada una de las estrategias se invoca en secuencia hasta que se maneja la solicitud. Entonces, en su ejemplo, tomaría el argumento más genérico, pero verifique si es el específico. Si es así, maneja la solicitud. De lo contrario, deja que el siguiente lo intente ...

Puente

Un patrón de puente puede ser apropiado aquí también. Esto es, en cierto sentido, similar al patrón de Estrategia, pero es diferente en el sentido de que una implementación puente seleccionaría la estrategia en el momento de la construcción, en lugar de hacerlo en tiempo de ejecución. Entonces construirías un "consumidor" diferente para cada implementación, con los detalles integrados en el interior como dependencias.

Patrón de visitante

Mencionaste el patrón de visitante en tu pregunta, así que me gustaría figurar aquí. No estoy seguro de que sea apropiado en este contexto, porque un visitante es muy similar a un patrón de estrategia diseñado para atravesar una estructura. Si no tiene una estructura de datos para atravesar, el patrón del visitante se destilará para que se vea bastante similar a un patrón de estrategia. Digo justamente, porque la dirección del control es diferente, pero la relación final es más o menos la misma.

Otros patrones

Al final, realmente depende del problema concreto que estás tratando de resolver. Si está tratando de manejar solicitudes HTTP, donde cada "Consumidor" maneja un tipo de solicitud diferente (XML vs HTML vs JSON, etc.), la mejor opción será muy diferente a si está intentando manejar la búsqueda del área geométrica de un polígono Claro, podría usar el mismo patrón para ambos, pero en realidad no son el mismo problema.

Dicho esto, el problema también podría resolverse con un Patrón de Mediador (en el caso de que múltiples "Consumidores" necesiten una oportunidad de procesar datos), un Patrón de Estado (en el caso donde el "Consumidor" dependerá de los datos consumidos en el pasado) o incluso un patrón de adaptador (en el caso de que esté abstrayendo un subsistema diferente en el consumidor especializado) ...

En resumen, es un problema difícil de responder, porque hay tantas soluciones que es difícil decir cuál es la correcta ...