He oído que el Principio de sustitución de Liskov (LSP) es un principio fundamental del diseño orientado a objetos. ¿Qué es y cuáles son algunos ejemplos de su uso?
908
He oído que el Principio de sustitución de Liskov (LSP) es un principio fundamental del diseño orientado a objetos. ¿Qué es y cuáles son algunos ejemplos de su uso?
Respuestas:
Un gran ejemplo que ilustra el LSP (dado por el tío Bob en un podcast que escuché recientemente) fue cómo a veces algo que suena bien en lenguaje natural no funciona en el código.
En matemáticas, a
Square
es aRectangle
. De hecho, es una especialización de un rectángulo. El "es un" hace que quieras modelar esto con herencia. Sin embargo, si en el código que hizoSquare
derivan deRectangle
, a continuación, unaSquare
debe estar en cualquier parte utilizable se espera unaRectangle
. Esto genera un comportamiento extraño.Imagina que tienes
SetWidth
ySetHeight
métodos en tuRectangle
clase base; Esto parece perfectamente lógico. Sin embargo, si suRectangle
referencia apunta a aSquare
, entonces,SetWidth
ySetHeight
no tiene sentido porque establecer uno cambiaría al otro para que coincida. En este caso,Square
falla la prueba de sustitución de LiskovRectangle
y la abstracción de haberSquare
heredadoRectangle
es mala.Todos deberían ver los otros valiosos carteles motivacionales de los Principios SÓLIDOS .
fuente
Square.setWidth(int width)
se implementara asíthis.width = width; this.height = width;
:? En este caso, se garantiza que el ancho es igual a la altura.El principio de sustitución de Liskov (LSP, lsp) es un concepto en Programación Orientada a Objetos que establece:
En esencia, LSP se trata de interfaces y contratos, así como de cómo decidir cuándo extender una clase versus usar otra estrategia, como la composición, para lograr su objetivo.
La manera eficaz la mayoría que he visto para ilustrar este punto estaba en cabeza primero OOA & D . Presentan un escenario en el que usted es desarrollador de un proyecto para crear un marco para juegos de estrategia.
Presentan una clase que representa un tablero que se ve así:
Todos los métodos toman las coordenadas X e Y como parámetros para ubicar la posición del mosaico en la matriz bidimensional de
Tiles
. Esto permitirá que un desarrollador de juegos administre unidades en el tablero durante el transcurso del juego.El libro continúa cambiando los requisitos para decir que el marco del juego también debe ser compatible con tableros de juegos 3D para acomodar juegos que tienen vuelo. Entonces
ThreeDBoard
se introduce una clase que se extiendeBoard
.A primera vista, esto parece una buena decisión.
Board
proporciona tanto elHeight
yWidth
propiedades yThreeDBoard
proporciona el eje Z.Cuando se descompone es cuando observa a todos los demás miembros heredados de
Board
. Los métodos paraAddUnit
,GetTile
,GetUnits
y así sucesivamente, todos toman ambos parámetros x e y en laBoard
clase pero laThreeDBoard
necesita un parámetro Z también.Entonces debe implementar esos métodos nuevamente con un parámetro Z. El parámetro Z no tiene contexto para la
Board
clase y los métodos heredados de laBoard
clase pierden su significado. Una unidad de código que intente usar laThreeDBoard
clase como su clase baseBoard
no tendrá mucha suerte.Tal vez deberíamos encontrar otro enfoque. En lugar de extenderse
Board
,ThreeDBoard
debe estar compuesto deBoard
objetos. UnBoard
objeto por unidad del eje Z.Esto nos permite utilizar buenos principios orientados a objetos como la encapsulación y la reutilización y no viola el LSP.
fuente
Hagamos un ejemplo simple en Java:
Mal ejemplo
El pato puede volar porque es un pájaro, pero ¿qué pasa con esto?
El avestruz es un pájaro, pero no puede volar, la clase de avestruz es un subtipo de la clase pájaro, pero no puede usar el método de volar, eso significa que estamos rompiendo el principio LSP.
Buen ejemplo
fuente
Bird bird
. Tienes que lanzar el objeto a FlyingBirds para hacer uso de la mosca, lo cual no es bueno, ¿verdad?Bird bird
, eso significa que no puede usarfly()
. Eso es. Pasar aDuck
no cambia este hecho. Si el cliente lo ha hechoFlyingBirds bird
, incluso si se superaDuck
, siempre debería funcionar de la misma manera.LSP se refiere a invariantes.
El ejemplo clásico está dado por la siguiente declaración de pseudocódigo (implementaciones omitidas):
Ahora tenemos un problema aunque la interfaz coincide. La razón es que hemos violado invariantes derivados de la definición matemática de cuadrados y rectángulos. La forma en que funcionan los captadores y los establecedores,
Rectangle
debe satisfacer lo siguiente invariante:Sin embargo, esta invariante debe ser violada por una implementación correcta de
Square
, por lo tanto, no es un sustituto válido deRectangle
.fuente
Robert Martin tiene un excelente artículo sobre el Principio de sustitución de Liskov . Discute formas sutiles y no tan sutiles de cómo se puede violar el principio.
Algunas partes relevantes del documento (tenga en cuenta que el segundo ejemplo está muy condensado):
fuente
Now the rule for the preconditions and postconditions for derivatives, as stated by Meyer is: ...when redefining a routine [in a derivative], you may only replace its precondition by a weaker one, and its postcondition by a stronger one.
si una precondición de clase infantil es más fuerte que una precondición de clase de padre, no podría sustituir a un niño por un padre sin violar la precondición. De ahí el LSP.El LSP es necesario cuando algún código cree que está llamando a los métodos de un tipo
T
, y sin saberlo puede llamar a los métodos de un tipoS
, dondeS extends T
(es decirS
, hereda, deriva o es un subtipo del supertipoT
).Por ejemplo, esto ocurre cuando una función con un parámetro de entrada de tipo
T
se llama (es decir, se invoca) con un valor de argumento de tipoS
. O, donde un identificador de tipoT
, se le asigna un valor de tipoS
.LSP requiere que las expectativas (es decir, invariantes) para los métodos de tipo
T
(pRectangle
. Ej. ) No se infrinjan cuando se invocan los métodos de tipoS
(pSquare
. Ej. ).Incluso un tipo con campos inmutables todavía tiene invariantes, por ejemplo, los establecedores de rectángulo inmutables esperan que las dimensiones se modifiquen de forma independiente, pero los establecedores cuadrados inmutables violan esta expectativa.
LSP requiere que cada método del subtipo
S
tenga parámetros de entrada contravariantes y una salida covariante.Contravariante significa que la varianza es contraria a la dirección de la herencia, es decir, el tipo
Si
de cada parámetro de entrada de cada método del subtipoS
debe ser el mismo o un supertipo del tipoTi
del parámetro de entrada correspondiente del método correspondiente del supertipoT
.Covarianza significa que la varianza está en la misma dirección de la herencia, es decir, el tipo
So
, de la salida de cada método del subtipoS
, debe ser el mismo o un subtipo del tipoTo
de la salida correspondiente del método correspondiente del supertipoT
.Esto se debe a que si la persona que llama cree que tiene un tipo
T
, cree que está llamando a un métodoT
, entonces proporciona argumentos de tipoTi
y asigna la salida al tipoTo
. Cuando realmente llama al método correspondiente deS
, entonces cadaTi
argumento de entrada se asigna a unSi
parámetro de entrada, y laSo
salida se asigna al tipoTo
. Por lo tanto, siSi
no fuera contravariante wrt aTi
, entoncesXi
seSi
podría asignar un subtipo , que no sería un subtipo deTi
.Además, para los idiomas (p. Ej., Scala o Ceilán) que tienen anotaciones de variación del sitio de definición en los parámetros de polimorfismo de tipo (es decir, genéricos), la codirección o contradirección de la anotación de variación para cada parámetro de tipo del tipo
T
debe ser opuesta o la misma dirección respectivamente a cada parámetro de entrada o salida (de cada método deT
) que tiene el tipo del parámetro de tipo.Además, para cada parámetro de entrada o salida que tiene un tipo de función, se invierte la dirección de varianza requerida. Esta regla se aplica de forma recursiva.
Subtipar es apropiado donde los invariantes pueden ser enumerados.
Hay mucha investigación en curso sobre cómo modelar invariantes, para que el compilador los aplique.
Typestate (consulte la página 3) declara y aplica invariantes de estado ortogonales para escribir. Alternativamente, los invariantes pueden hacerse cumplir mediante la conversión de aserciones a tipos . Por ejemplo, para afirmar que un archivo está abierto antes de cerrarlo, File.open () podría devolver un tipo OpenFile, que contiene un método close () que no está disponible en File. Una API tic-tac-toe puede ser otro ejemplo de empleo de tipeo para imponer invariantes en tiempo de compilación. El sistema de tipos puede incluso ser Turing completo, por ejemplo, Scala . Los lenguajes y demostradores de teoremas de tipo dependiente formalizan los modelos de mecanografía de orden superior.
Debido a la necesidad de que la semántica abstraiga sobre la extensión , espero que el empleo de la tipificación para modelar invariantes, es decir, una semántica denotativa de orden superior unificada, sea superior al Typestate. 'Extensión' significa la composición ilimitada y permutada del desarrollo modular no coordinado. Porque me parece ser la antítesis de la unificación y, por lo tanto, los grados de libertad, tener dos modelos mutuamente dependientes (por ejemplo, tipos y Typestate) para expresar la semántica compartida, que no puede unificarse entre sí para una composición extensible . Por ejemplo, la extensión similar a un problema de expresión se unificó en los dominios de subtipo, sobrecarga de funciones y tipado paramétrico.
Mi posición teórica es que para que exista el conocimiento (ver sección “La centralización es ciega y no apta”), nunca habrá un modelo general que pueda exigir una cobertura del 100% de todos los posibles invariantes en un lenguaje informático completo de Turing. Para que exista el conocimiento, existen muchas posibilidades inesperadas, es decir, el desorden y la entropía siempre deben estar aumentando. Esta es la fuerza entrópica. Probar todos los cálculos posibles de una extensión potencial, es calcular a priori todas las extensiones posibles.
Es por eso que existe el Teorema de detención, es decir, es indecidible si todos los programas posibles en un lenguaje de programación completo de Turing terminan. Se puede demostrar que algún programa específico termina (uno en el que se han definido y calculado todas las posibilidades). Pero es imposible demostrar que toda la extensión posible de ese programa finaliza, a menos que las posibilidades de extensión de ese programa no estén completas (por ejemplo, a través del tipeo dependiente). Dado que el requisito fundamental para la integridad de Turing es la recursión ilimitada , es intuitivo comprender cómo los teoremas de incompletitud de Gödel y la paradoja de Russell se aplican a la extensión.
Una interpretación de estos teoremas los incorpora en una comprensión conceptual generalizada de la fuerza entrópica:
fuente
Veo rectángulos y cuadrados en cada respuesta, y cómo violar el LSP.
Me gustaría mostrar cómo se puede conformar el LSP con un ejemplo del mundo real:
Este diseño se ajusta al LSP porque el comportamiento permanece sin cambios, independientemente de la implementación que elijamos usar.
Y sí, puede violar LSP en esta configuración haciendo un cambio simple de esta manera:
Ahora los subtipos no se pueden usar de la misma manera, ya que ya no producen el mismo resultado.
fuente
Database::selectQuery
admitir solo el subconjunto de SQL compatible con todos los motores de base de datos. Eso no es práctico ... Dicho esto, el ejemplo es aún más fácil de entender que la mayoría de los demás utilizados aquí.Hay una lista de verificación para determinar si usted está violando o no a Liskov.
Lista de Verificación:
Restricción de historial : al anular un método, no está permitido modificar una propiedad no modificable en la clase base. Eche un vistazo a este código y podrá ver que el Nombre se define como no modificable (conjunto privado), pero SubType presenta un nuevo método que permite modificarlo (mediante reflexión):
Hay otros 2 elementos: contravarianza de los argumentos del método y covarianza de los tipos de retorno . Pero no es posible en C # (soy un desarrollador de C #), así que no me importan.
Referencia:
fuente
El LSP es una regla sobre el contrato de las clases: si una clase base cumple un contrato, entonces las clases derivadas del LSP también deben cumplir ese contrato.
En pseudo-pitón
satisface LSP si cada vez que llama a Foo en un objeto Derivado, da exactamente los mismos resultados que llamar a Foo en un objeto Base, siempre que arg sea el mismo.
fuente
2 + "2"
). ¿Quizás confunde "fuertemente tipado" con "tipado estáticamente"?En pocas palabras, dejemos rectángulos rectángulos y cuadrados cuadrados, ejemplo práctico al extender una clase principal, debe PRESERVAR la API principal exacta o EXTENDERLA.
Digamos que tiene una base de ItemsRepository.
Y una subclase que lo extiende:
Entonces podría tener un Cliente trabajando con la API Base ItemsRepository y confiando en ella.
El LSP se rompe cuando la sustitución de la clase padre con una subclase rompe el contrato de la API .
Puede obtener más información sobre cómo escribir software mantenible en mi curso: https://www.udemy.com/enterprise-php/
fuente
Cuando leí por primera vez acerca de LSP, supuse que se refería a esto en un sentido muy estricto, esencialmente equiparándolo con la implementación de la interfaz y la conversión de tipo seguro. Lo que significaría que el lenguaje en sí garantiza o no el LSP. Por ejemplo, en este sentido estricto, ThreeDBoard es ciertamente sustituible por Board, en lo que respecta al compilador.
Después de leer más sobre el concepto, descubrí que el LSP generalmente se interpreta de manera más amplia que eso.
En resumen, lo que significa que el código del cliente "sepa" que el objeto detrás del puntero es de un tipo derivado en lugar del tipo de puntero no está restringido a la seguridad de tipo. La adherencia al LSP también es comprobable mediante el sondeo del comportamiento real de los objetos. Es decir, examinar el impacto del estado de un objeto y los argumentos del método en los resultados de las llamadas al método, o los tipos de excepciones lanzadas desde el objeto.
Volviendo al ejemplo nuevamente, en teoría, los métodos de la Junta pueden funcionar bien en ThreeDBoard. Sin embargo, en la práctica, será muy difícil evitar diferencias de comportamiento que el cliente no pueda manejar adecuadamente, sin obstaculizar la funcionalidad que ThreeDBoard pretende agregar.
Con este conocimiento en mano, evaluar la adherencia a LSP puede ser una gran herramienta para determinar cuándo la composición es el mecanismo más apropiado para extender la funcionalidad existente, en lugar de la herencia.
fuente
Supongo que todo el mundo cubrió lo que LSP es técnicamente: básicamente desea poder abstraerse de los detalles del subtipo y usar supertipos de manera segura.
Entonces Liskov tiene 3 reglas subyacentes:
Regla de firma: debe haber una implementación válida de cada operación del supertipo en el subtipo sintácticamente. Algo que un compilador podrá verificar por usted. Hay una pequeña regla sobre lanzar menos excepciones y ser al menos tan accesible como los métodos de supertipo.
Regla de métodos: La implementación de esas operaciones es semánticamente sólida.
Regla de propiedades: esto va más allá de las llamadas a funciones individuales.
Todas estas propiedades deben conservarse y la funcionalidad adicional de subtipo no debe violar las propiedades de supertipo.
Si se resuelven estas tres cosas, se ha abstraído de las cosas subyacentes y está escribiendo código débilmente acoplado.
Fuente: Desarrollo de programas en Java - Barbara Liskov
fuente
Un ejemplo importante del uso de LSP es en las pruebas de software .
Si tengo una clase A que es una subclase de B que cumple con LSP, entonces puedo reutilizar el conjunto de pruebas de B para probar A.
Para probar completamente la subclase A, probablemente necesito agregar algunos casos de prueba más, pero como mínimo puedo reutilizar todos los casos de prueba de la superclase B.
Una forma de darse cuenta es esto construyendo lo que McGregor llama una "jerarquía paralela para las pruebas": mi
ATest
clase heredará deBTest
. Se necesita alguna forma de inyección para garantizar que el caso de prueba funcione con objetos de tipo A en lugar de tipo B (un patrón de método de plantilla simple funcionará).Tenga en cuenta que reutilizar el conjunto de superpruebas para todas las implementaciones de subclase es, de hecho, una forma de comprobar que estas implementaciones de subclase son compatibles con LSP. Por lo tanto, también se puede argumentar que se debe ejecutar el conjunto de pruebas de superclase en el contexto de cualquier subclase.
Consulte también la respuesta a la pregunta de Stackoverflow " ¿Puedo implementar una serie de pruebas reutilizables para probar la implementación de una interfaz? "
fuente
Vamos a ilustrar en Java:
No hay problema aquí, ¿verdad? Un automóvil es definitivamente un dispositivo de transporte, y aquí podemos ver que anula el método startEngine () de su superclase.
Agreguemos otro dispositivo de transporte:
¡Todo no está yendo según lo planeado ahora! Sí, una bicicleta es un dispositivo de transporte, sin embargo, no tiene un motor y, por lo tanto, el método startEngine () no se puede implementar.
La solución a estos problemas es una jerarquía de herencia correcta, y en nuestro caso resolveríamos el problema diferenciando las clases de dispositivos de transporte con y sin motores. Aunque una bicicleta es un dispositivo de transporte, no tiene motor. En este ejemplo, nuestra definición de dispositivo de transporte es incorrecta. No debe tener un motor.
Podemos refactorizar nuestra clase TransportationDevice de la siguiente manera:
Ahora podemos extender el dispositivo de transporte para dispositivos no motorizados.
Y extienda el dispositivo de transporte para dispositivos motorizados. Aquí es más apropiado agregar el objeto Motor.
Por lo tanto, nuestra clase de automóviles se vuelve más especializada, mientras se adhiere al Principio de sustitución de Liskov.
Y nuestra clase de bicicletas también cumple con el Principio de sustitución de Liskov.
fuente
Esta formulación del LSP es demasiado fuerte:
Lo que básicamente significa que S es otra implementación completamente encapsulada de exactamente lo mismo que T. Y podría ser audaz y decidir que el rendimiento es parte del comportamiento de P ...
Entonces, básicamente, cualquier uso de enlace tardío viola el LSP. ¡El objetivo de OO es obtener un comportamiento diferente cuando sustituimos un objeto de un tipo por otro de otro tipo!
La formulación citada por wikipedia es mejor ya que la propiedad depende del contexto y no necesariamente incluye todo el comportamiento del programa.
fuente
En una oración muy simple, podemos decir:
La clase secundaria no debe violar sus características de clase base. Debe ser capaz con eso. Podemos decir que es lo mismo que subtipar.
fuente
Ejemplo:
A continuación se muestra el ejemplo clásico por el cual se viola el Principio de sustitución de Liskov. En el ejemplo, se usan 2 clases: Rectángulo y Cuadrado. Supongamos que el objeto Rectangle se usa en algún lugar de la aplicación. Extendemos la aplicación y agregamos la clase Square. La clase cuadrada es devuelta por un patrón de fábrica, basado en algunas condiciones y no sabemos exactamente qué tipo de objeto será devuelto. Pero sabemos que es un rectángulo. Obtenemos el objeto rectángulo, establecemos el ancho en 5 y la altura en 10 y obtenemos el área. Para un rectángulo con ancho 5 y altura 10, el área debe ser 50. En cambio, el resultado será 100
Ver también: Abrir Cerrar Principio
Algunos conceptos similares para una mejor estructura: Convención sobre configuración
fuente
El principio de sustitución de Liskov
fuente
Algún apéndice:
Me pregunto por qué nadie escribió acerca de las invariantes, condiciones previas y condiciones de publicación de la clase base que las clases derivadas deben obedecer. Para que una clase D derivada sea completamente sustituible por la clase B básica, la clase D debe obedecer ciertas condiciones:
Por lo tanto, el derivado debe tener en cuenta las tres condiciones anteriores impuestas por la clase base. Por lo tanto, las reglas de subtipo están predeterminadas. Lo que significa que la relación 'IS A' se obedecerá solo cuando el subtipo obedezca ciertas reglas. Estas reglas, en forma de invariantes, precondiciones y condiciones posteriores, deben decidirse mediante un ' contrato de diseño ' formal .
Más discusiones sobre esto disponibles en mi blog: Principio de sustitución de Liskov
fuente
El LSP en términos simples establece que los objetos de la misma superclase deben poder intercambiarse entre sí sin romper nada.
Por ejemplo, si tenemos una
Cat
y unaDog
clase derivada de unaAnimal
clase, cualquier función que use la clase Animal debería poder usarCat
oDog
comportarse normalmente.fuente
¿Sería útil implementar ThreeDBoard en términos de una matriz de placa?
Tal vez desee tratar las rebanadas de ThreeDBoard en varios planos como un tablero. En ese caso, es posible que desee abstraer una interfaz (o clase abstracta) para que Board permita múltiples implementaciones.
En términos de interfaz externa, es posible que desee factorizar una interfaz de placa para TwoDBoard y ThreeDBoard (aunque ninguno de los métodos anteriores se ajusta).
fuente
Un cuadrado es un rectángulo donde el ancho es igual a la altura. Si el cuadrado establece dos tamaños diferentes para el ancho y la altura, viola el cuadrado invariante. Esto se soluciona mediante la introducción de efectos secundarios. Pero si el rectángulo tenía un setSize (alto, ancho) con la condición previa 0 <alto y 0 <ancho. El método del subtipo derivado requiere height == width; una precondición más fuerte (y eso viola lsp). Esto muestra que aunque el cuadrado es un rectángulo, no es un subtipo válido porque la condición previa se fortalece. La solución (en general, algo malo) causa un efecto secundario y esto debilita la condición posterior (que viola lsp). setWidth en la base tiene la condición de publicación 0 <ancho. El derivado lo debilita con altura == ancho.
Por lo tanto, un cuadrado redimensionable no es un rectángulo redimensionable.
fuente
Este principio fue introducido por Barbara Liskov en 1987 y extiende el Principio Abierto-Cerrado al enfocarse en el comportamiento de una superclase y sus subtipos.
Su importancia se hace evidente cuando consideramos las consecuencias de violarlo. Considere una aplicación que usa la siguiente clase.
Imagine que un día, el cliente exige la capacidad de manipular cuadrados además de rectángulos. Como un cuadrado es un rectángulo, la clase de cuadrado debe derivarse de la clase Rectangle.
Sin embargo, al hacerlo nos encontraremos con dos problemas:
Un cuadrado no necesita variables de altura y ancho heredadas del rectángulo y esto podría generar un desperdicio significativo en la memoria si tenemos que crear cientos de miles de objetos cuadrados. Las propiedades de establecimiento de ancho y alto heredadas del rectángulo no son apropiadas para un cuadrado ya que el ancho y alto de un cuadrado son idénticos. Para establecer el alto y el ancho en el mismo valor, podemos crear dos nuevas propiedades de la siguiente manera:
Ahora, cuando alguien establece el ancho de un objeto cuadrado, su altura cambiará en consecuencia y viceversa.
Avancemos y consideremos esta otra función:
Si pasamos una referencia a un objeto cuadrado en esta función, violaríamos el LSP porque la función no funciona para derivadas de sus argumentos. El ancho y la altura de las propiedades no son polimórficos porque no se declaran virtuales en rectángulo (el objeto cuadrado se dañará porque la altura no se cambiará).
Sin embargo, al declarar que las propiedades del configurador son virtuales, enfrentaremos otra violación, el OCP. De hecho, la creación de un cuadrado de clase derivado está causando cambios en el rectángulo de la clase base.
fuente
La explicación más clara para LSP que encontré hasta ahora ha sido "El Principio de sustitución de Liskov dice que el objeto de una clase derivada debería ser capaz de reemplazar un objeto de la clase base sin traer ningún error en el sistema o modificar el comportamiento de la clase base "desde aquí . El artículo da un ejemplo de código para violar LSP y arreglarlo.
fuente
Digamos que usamos un rectángulo en nuestro código
En nuestra clase de geometría aprendimos que un cuadrado es un tipo especial de rectángulo porque su ancho tiene la misma longitud que su altura. Hagamos una
Square
clase también basada en esta información:Si reemplazamos el
Rectangle
conSquare
en nuestro primer código, entonces se romperá:Esto es porque el
Square
tiene una nueva condición de que no tenemos en laRectangle
clase:width == height
. Según LSP, lasRectangle
instancias deben ser sustituibles porRectangle
instancias de subclase. Esto se debe a que estas instancias pasan la verificación de tipo paraRectangle
instancias y, por lo tanto, causarán errores inesperados en su código.Este fue un ejemplo para la parte de "precondiciones que no pueden fortalecerse en un subtipo" en el artículo wiki . En resumen, violar LSP probablemente causará errores en su código en algún momento.
fuente
LSP dice que "los objetos deben ser reemplazables por sus subtipos". Por otro lado, este principio apunta a
y el siguiente ejemplo ayuda a comprender mejor el LSP.
Sin LSP:
Fijación por LSP:
fuente
Le animo a leer el artículo: Violar el principio de sustitución de Liskov (LSP) .
Puede encontrar una explicación sobre el Principio de sustitución de Liskov, pistas generales que lo ayudarán a adivinar si ya lo ha violado y un ejemplo de enfoque que lo ayudará a hacer que su jerarquía de clases sea más segura.
fuente
EL PRINCIPIO DE SUSTITUCIÓN DE LISKOV (del libro de Mark Seemann) establece que deberíamos poder reemplazar una implementación de una interfaz con otra sin romper el cliente o la implementación. Es este principio el que permite abordar los requisitos que se produzcan en el futuro, incluso si podemos ' No los preveo hoy.
Si desconectamos la computadora de la pared (Implementación), ni la toma de corriente de la pared (Interfaz) ni la computadora (Cliente) se descomponen (de hecho, si es una computadora portátil, incluso puede funcionar con sus baterías por un período de tiempo) . Sin embargo, con el software, un cliente a menudo espera que un servicio esté disponible. Si se eliminó el servicio, obtenemos una NullReferenceException. Para hacer frente a este tipo de situación, podemos crear una implementación de una interfaz que no haga "nada". Este es un patrón de diseño conocido como Objeto Nulo, [4] y corresponde aproximadamente a desenchufar la computadora de la pared. Debido a que estamos utilizando un acoplamiento flexible, podemos reemplazar una implementación real con algo que no hace nada sin causar problemas.
fuente
El Principio de sustitución de Likov establece que si un módulo de programa está usando una clase Base, entonces la referencia a la clase Base se puede reemplazar con una clase Derivada sin afectar la funcionalidad del módulo del programa.
Intención: los tipos derivados deben ser completamente sustitutos de sus tipos base.
Ejemplo: tipos de retorno covariantes en java.
fuente
Aquí hay un extracto de esta publicación que aclara las cosas muy bien:
[..] para comprender algunos principios, es importante darse cuenta de cuándo se ha violado. Esto es lo que haré ahora.
¿Qué significa la violación de este principio? Implica que un objeto no cumple el contrato impuesto por una abstracción expresada con una interfaz. En otras palabras, significa que identificó mal sus abstracciones.
Considere el siguiente ejemplo:
¿Es esto una violación de LSP? Si. Esto se debe a que el contrato de la cuenta nos dice que se retiraría una cuenta, pero este no es siempre el caso. Entonces, ¿qué debo hacer para solucionarlo? Acabo de modificar el contrato:
Voilà, ahora el contrato está satisfecho.
Esta violación sutil a menudo impone a un cliente la capacidad de distinguir entre los objetos concretos empleados. Por ejemplo, dado el contrato de la primera cuenta, podría tener el siguiente aspecto:
Y, esto viola automáticamente el principio abierto-cerrado [es decir, para el requisito de retiro de dinero. Porque nunca se sabe lo que sucede si un objeto que viola el contrato no tiene suficiente dinero. Probablemente simplemente no devuelve nada, probablemente se lanzará una excepción. Entonces tienes que comprobar si
hasEnoughMoney()
, lo que no forma parte de una interfaz. Entonces, esta verificación forzada dependiente de la clase concreta es una violación de OCP].Este punto también aborda una idea errónea que encuentro con bastante frecuencia sobre la violación de LSP. Dice "si el comportamiento de un padre cambió en un niño, entonces viola el LSP". Sin embargo, no lo hace, siempre y cuando un niño no viole el contrato de sus padres.
fuente