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 elConsumer
". Ok, eso tiene sentido, porque la declaración del método es tan buena como la firma. Semánticamente, aunque la ausencia deconsume()
, 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 clasePerson
,Child
. Es decir, unChild
es una persona. Entonces, uno podría querer hacer una subclase deDoctor
,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 unChild
es unaPerson
,Child::see(d)
debe llevar a cualquierDoctor
, no solo a unPediatrician
.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 ...