2009-12-13 10 views
20

Me va a utilizar el siguiente ejemplo para ilustrar mi pregunta:¿Hay alguna forma de redefinir una sugerencia de tipo a una clase descendiente al extender una clase abstracta?

class Attribute {} 

class SimpleAttribute extends Attribute {} 



abstract class AbstractFactory { 
    abstract public function update(Attribute $attr, $data); 
} 

class SimpleFactory extends AbstractFactory { 
    public function update(SimpleAttribute $attr, $data); 

} 

Si intenta ejecutar esto, PHP arrojará un error fatal, diciendo que el Declaration of SimpleFactory::update() must be compatible with that of AbstractFactory::update()

entiendo exactamente lo que esto significa: La firma del método SimpleFactory::update() s debe coincidir exactamente con la de su clase abstracta principal.

Sin embargo, mi pregunta: ¿hay alguna manera de permitir que el método concreto (en este caso, SimpleFactory::update()) redefina la sugerencia de tipo a un descendiente válido de la pista original?

Un ejemplo podría ser el operador instanceof, que devolvería cierto en el caso siguiente:

SimpleAttribute instanceof Attribute // => true 

Soy consciente de que a medida que una solución alternativa, que podría hacer que el tipo de indicio de la misma en el método concreto, y haga una instancia de verificación en el propio cuerpo del método, pero ¿hay alguna manera de simplemente hacer cumplir esto en el nivel de la firma?

+3

+1 Me encantaría ver si esto también es posible, aunque por mi vida no puedo entender cómo. Tal vez PHP6 tendrá soporte mejorado para la sobrecarga de métodos y detectará múltiples firmas de métodos *, por lo que 'actualización de función pública abstracta (atributo $ attr, $ data)' y 'actualización de función pública abstracta (SimpleAttribute $ attr, $ data)' podría co- existir en el mismo alcance de clase – leepowers

Respuesta

15

No me lo esperaba, ya que puede romper los contratos tipo de alusión. Supongamos que una función foo tomó una AbstractFactory y se pasó a SimpleFactory.

function foo(AbstractFactory $maker) { 
    $attr = new Attribute(); 
    $maker->update($attr, 42); 
} 
... 
$packager=new SimpleFactory(); 
foo($packager); 

foo llamadas update y pasa un atributo a la fábrica, que se debe tomar debido a que la firma del método AbstractFactory::update promete que puede tomar un atributo. Bam! SimpleFactory tiene un objeto de tipo que no puede manejar correctamente.

class Attribute {} 
class SimpleAttribute extends Attribute { 
    public function spin() {...} 
} 
class SimpleFactory extends AbstractFactory { 
    public function update(SimpleAttribute $attr, $data) { 
     $attr->spin(); // This will fail when called from foo() 
    } 
} 

En la terminología de contrato, las clases descendientes deben cumplir los contratos de sus antepasados, lo que significa que los parámetros de función puede obtener más basales/menos especificado/oferta un contrato más débil y devolver valores pueden ser más derivan/más se ha especificado/ofrecer una contrato más fuerte. El principio se describe para Eiffel (posiblemente el lenguaje de diseño por contrato más popular) en "An Eiffel Tutorial: Inheritance and Contracts". El debilitamiento y el fortalecimiento de los tipos son ejemplos de contravariance and covariance, respectivamente.

En términos más teóricos, este es un ejemplo de violación de LSP. No, no queLSP; el Liskov Substitution Principle, que establece que los objetos de un subtipo pueden sustituirse por objetos de un supertipo. SimpleFactory es un subtipo de AbstractFactory, y foo toma un AbstractFactory. Por lo tanto, de acuerdo con LSP, foo debe tomar un SimpleFactory. Si lo hace, se produce un error fatal de "Llamada a un método no definido", lo que significa que se ha violado el LSP.

+0

Pero, su primer caso nunca ocurriría porque 'foo' no aceptará una instancia de' SimpleFactory'. Mi pregunta se inclinaba más hacia por qué no puedo hacer el contrato del método 'update' _more strict_ en la fábrica de concreto. – jason

+1

El caso de uso es que hay varias fábricas de hormigón, digamos 'SimpleFactory',' EnumberableFactory' y así sucesivamente, cada una de las cuales está destinada simplemente a operar en su propio tipo de atributo, 'SimpleAttribute' y' EnumerableAttribute', respectivamente. Pero, tal como está, dado que no puedo ajustar la restricción de sugerencia en la subclase de fábrica, todas las fábricas de hormigón tendrán que aceptar todos los tipos de atributos ... salvo una simple comprobación en la parte superior del cuerpo del método, que es bien, pero me preguntaba si era posible en el nivel de la firma. – jason

+0

Aún así, realmente aprecio tu respuesta. – jason

3

La respuesta aceptada es correcta al responder al OP que lo que intentó violar el Principio de sustitución de Liskov. Los OP deben utilizar una nueva interfaz y usar composición en lugar de herencia para resolver el problema de la firma de su función. El cambio en el problema de ejemplo de OP es bastante menor.

class Attribute {} 

class SimpleAttribute extends Attribute {} 

abstract class AbstractFactory { 
    abstract public function update(Attribute $attr, $data); 
} 

interface ISimpleFactory { 
    function update(SimpleAttribute $attr, $data); 
} 

class SimpleFactory implements ISimpleFactory { 
    private $factory; 
    public function __construct(AbstractFactory $factory) 
    { 
     $this->factory = $factory; 
    } 
    public function update(SimpleAttribute $attr, $data) 
    { 
     $this->factory->update($attr, $data); 
    } 

} 

El ejemplo de código anterior hace dos cosas: 1) crea la interfaz ISimpleFactory que todo el código dependiente de una fábrica para SimpleAttributes codificaría contra 2) de aplicación SimpleFactory usando una fábrica genérico requeriría SimpleFactory tomar a través del un constructor instancia de la clase derivada de AbstractFactory que luego usaría en la función de actualización desde el método de interfaz ISimpleFactory anulado en SimpleFactory.

Esto permite encapsular cualquier dependencia en una fábrica genérica de cualquier código que dependiera de ISimpleFactory, pero permitió a SimpleFactory sustituir cualquier fábrica derivada de AbstractFactory (LSP Satisfactorio) sin tener que cambiar ninguno de sus códigos (el código de llamada proporcionar la dependencia). Una nueva clase derivada de ISimpleFactory puede decidir no utilizar ninguna instancia derivada de AbstractFactory para implementarse, y todo el código de llamada estaría protegido de ese detalle.

La herencia puede ser de gran valor, sin embargo, a veces la Composición se pasa por alto y es mi forma preferida de reducir el acoplamiento apretado.

+0

¿Qué pasaría al constructor por 'SimpleFactory' en su implementación? ¿Una instancia de otra clase de fábrica que _ hereda_ hereda de 'AbstractFactory'? –

+0

Sí, si se sigue estrictamente mi ejemplo, pasará una instancia de otra clase de fábrica que hereda de AbstractFactory. En mi ejemplo, SimpleFactory se implementa con AbstractFactory, sin embargo, esto no es necesariamente así, ISimpleFactory podría implementarse sin AbstractFactory si se desea. Mi ejemplo solo muestra que puedes ocultar ese detalle de implementación, revelado solo por el código de llamada a través de la inyección del constructor. –

Cuestiones relacionadas