¿Los casos especiales con retrocesos violan el Principio de sustitución de Liskov?

20

Digamos que tengo una interfaz FooInterfaceque tiene la siguiente firma:

interface FooInterface {
    public function doSomething(SomethingInterface something);
}

Y una clase concreta ConcreteFooque implementa esa interfaz:

class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
    }

}

Me gustaría ConcreteFoo::doSomething()hacer algo único si se pasa un tipo especial de SomethingInterfaceobjeto (digamos que se llama SpecialSomething).

Definitivamente es una violación de LSP si fortalezco las condiciones previas del método o lanzo una nueva excepción, pero ¿seguiría siendo una violación de LSP si coloco SpecialSomethingobjetos especiales al tiempo que proporciono una reserva para SomethingInterfaceobjetos genéricos ? Algo como:

class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
        if (something instanceof SpecialSomething) {
            // Do SpecialSomething magic
        }
        else {
            // Do generic SomethingInterface magic
        }
    }

}
Evan
fuente

Respuestas:

19

Que podría ser una violación de la LSP dependiendo lo demás está en el contrato para el doSomethingmétodo de. Pero es casi seguro que huele a código incluso si no viola el LSP.

Por ejemplo, si parte del contrato de doSomethinges que llamará something.commitUpdates()al menos una vez antes de regresar, y para el caso especial al que llama commitSpecialUpdates(), entonces eso es una violación de LSP. Incluso si SpecialSomethingel commitSpecialUpdates()método fue diseñado conscientemente para hacer todas las mismas cosas que commitUpdates(), eso es solo piratear preventivamente la violación de LSP, y es exactamente el tipo de piratería que uno no tendría que hacer si uno siguiera LSP de manera consistente. Si algo como esto se aplica a su caso es algo que tendrá que averiguar comprobando su contrato para ese método (ya sea explícito o implícito).

La razón por la que se trata de un olor a código es porque verificar el tipo concreto de uno de sus argumentos pierde el punto de definir una interfaz / tipo abstracto para él en primer lugar, y porque, en principio, ya no puede garantizar que el método funcione (imagine si alguien escribe una subclase de SpecialSomethingcon el supuesto de que commitUpdates()se llamará). En primer lugar, intente hacer que estas actualizaciones especiales funcionen dentro de las existentes.SomethingInterface; Ese es el mejor resultado posible. Si está realmente seguro de que no puede hacer eso, entonces necesita actualizar la interfaz. Si no controla la interfaz, es posible que deba considerar escribir su propia interfaz que haga lo que desea. Si ni siquiera puede encontrar una interfaz que funcione para todos ellos, tal vez debería descartar la interfaz por completo y tener múltiples métodos que tomen diferentes tipos concretos, o tal vez sea necesario un refactor aún más grande. Tendríamos que saber más sobre la magia que ha comentado para saber cuál de estos es apropiado.

Ixrec
fuente
¡Gracias! Esto ayuda. Hipotéticamente, el propósito del doSomething()método sería la conversión de tipo a SpecialSomething: si lo recibe SpecialSomething, simplemente devolverá el objeto sin modificar, mientras que si recibe un SomethingInterfaceobjeto genérico , ejecutará un algoritmo para convertirlo en un SpecialSomethingobjeto. Como las condiciones previas y posteriores siguen siendo las mismas, no creo que se haya violado el contrato.
Evan
1
@Evan Oh wow ... ese es un caso interesante. Eso podría ser realmente sin problemas. Lo único en lo que puedo pensar es que si está devolviendo el objeto existente en lugar de construir un objeto nuevo, tal vez alguien dependa de que este método devuelva un objeto nuevo ... pero si ese tipo de cosas pueden romperse, las personas podrían depender de el idioma. ¿Es posible que alguien a quien llamar y = doSomething(x), a continuación x.setFoo(3), y luego encontrar que los y.getFoo()retornos 3?
Ixrec
Eso es problemático dependiendo del idioma, aunque debería solucionarse fácilmente simplemente devolviendo una copia del SpecialSomethingobjeto. Aunque en aras de la pureza, también pude ver renunciar a la optimización de casos especiales cuando el objeto pasado es SpecialSomethingy simplemente ejecutarlo a través del algoritmo de conversión más grande, ya que aún debería funcionar debido a que también es un SomethingInterfaceobjeto.
Evan
1

No es una violación del LSP. Sin embargo, sigue siendo una violación de la regla "no hacer verificaciones de tipo". Sería mejor diseñar el código de tal manera que el especial surja naturalmente; tal vez SomethingInterfacenecesite otro miembro que pueda lograr esto, o tal vez necesite inyectar una fábrica abstracta en alguna parte.

Sin embargo, esta no es una regla difícil y rápida, por lo que debe decidir si la compensación vale la pena. En este momento tiene un olor a código y un posible obstáculo para futuras mejoras. Deshacerse de él podría significar una arquitectura considerablemente más complicada. Sin más información no puedo decir cuál es mejor.

Sebastian Redl
fuente
1

No, aprovechar el hecho de que un argumento dado no solo proporciona la interfaz A, sino también A2, no viola el LSP.

Solo asegúrese de que el camino especial no tenga condiciones previas más fuertes (aparte de las probadas al decidir tomarlo), ni condiciones posteriores más débiles.

Las plantillas de C ++ a menudo lo hacen para proporcionar un mejor rendimiento, por ejemplo, requiriendo InputIterators, pero dando garantías adicionales si se llaman con RandomAccessIterators.

Si tiene que decidir en tiempo de ejecución en su lugar (por ejemplo, utilizando el casting dinámico), tenga cuidado con la decisión de qué camino tomar consumiendo todas sus ganancias potenciales o incluso más.

Aprovechar el caso especial a menudo va en contra de DRY (no se repita), ya que podría tener que duplicar el código, y en contra de KISS (Keep it Simple), ya que es más complejo.

Deduplicador
fuente
0

Existe una compensación entre el principio de "no hacer verificaciones de tipo" y "segregar sus interfaces". Si muchas clases proporcionan un medio viable pero posiblemente ineficiente para realizar algunas tareas, y algunas de ellas también pueden ofrecer un medio mejor, y existe la necesidad de un código que pueda aceptar cualquiera de la categoría más amplia de elementos que pueden realizar la tarea (tal vez de manera ineficiente), pero luego realice la tarea de la manera más eficiente posible, será necesario que todos los objetos implementen una interfaz que incluya un miembro para decir si el método más eficiente es compatible y otro para usarlo si es así, o de lo contrario haga que el código que recibe el objeto compruebe si admite una interfaz extendida y, si es así, lo emite.

Personalmente, estoy a favor del enfoque anterior, aunque deseo que los marcos orientados a objetos como .NET permitan que las interfaces especifiquen métodos predeterminados (haciendo que las interfaces más grandes sean menos dolorosas para trabajar). Si la interfaz común incluye métodos opcionales, entonces una sola clase de contenedor puede manejar objetos con muchas combinaciones diferentes de habilidades, al tiempo que promete a los consumidores solo aquellas habilidades presentes en el objeto envuelto original. Si muchas funciones se dividen en diferentes interfaces, entonces se necesitará un objeto contenedor diferente para cada combinación diferente de interfaces que los objetos envueltos puedan necesitar soportar.

Super gato
fuente
0

El principio de sustitución de Liskov se refiere a los subtipos que actúan de acuerdo con un contrato de su supertipo. Entonces, como escribió Ixrec, no hay suficiente información para responder si se trata de una violación de LSP.

Sin embargo, lo que se viola aquí es un principio abierto cerrado. Si tiene un nuevo requisito, hacer algo especial, y tiene que modificar el código existente, definitivamente está violando OCP .

Zapadlo
fuente