Estaba siguiendo esta pregunta muy votada sobre la posible violación del principio de sustitución de Liskov. Sé cuál es el principio de sustitución de Liskov, pero lo que aún no está claro en mi mente es qué podría salir mal si yo, como desarrollador, no pienso en el principio mientras escribo código orientado a objetos.
27
Respuestas:
Creo que se dice muy bien en esa pregunta, que es una de las razones por las que se votó tan bien.
Imagina si vas a:
En este método, ocasionalmente la llamada .Close () explotará, por lo que ahora, en función de la implementación concreta de un tipo derivado, debe cambiar la forma en que este método se comporta de cómo se escribiría este método si Task no tuviera subtipos que pudieran ser entregado a este método.
Debido a violaciones de la sustitución de liskov, el código que usa su tipo deberá tener un conocimiento explícito del funcionamiento interno de los tipos derivados para tratarlos de manera diferente. Esto combina estrechamente el código y, en general, hace que la implementación sea más difícil de usar de manera consistente.
fuente
Si no cumple con el contrato que se ha definido en la clase base, las cosas pueden fallar silenciosamente cuando obtiene resultados que están desactivados.
LSP en estados de Wikipedia
Si alguno de estos no se cumple, la persona que llama puede obtener un resultado que no espera.
fuente
Considere un caso clásico de los anales de las preguntas de la entrevista: ha derivado Circle de Ellipse. ¿Por qué? Porque un círculo es una elipse AN-AN, por supuesto
Excepto ... elipse tiene dos funciones:
Claramente, estos deben ser redefinidos para Circle, porque un Circle tiene un radio uniforme. Tienes dos posibilidades:
La mayoría de los idiomas OO no admiten el segundo, y por una buena razón: sería sorprendente descubrir que su círculo ya no es un círculo. Entonces la primera opción es la mejor. Pero considere la siguiente función:
Imagine que alguna función llama a e.set_alpha_radius. Pero debido a que e era realmente un Círculo, sorprendentemente también tiene establecido su radio beta.
Y aquí radica el principio de sustitución: una subclase debe ser sustituible por una superclase. De lo contrario, suceden cosas sorprendentes.
fuente
En palabras simples:
Su código tendrá una gran cantidad de cláusulas CASE / switch por todas partes.
Cada una de esas cláusulas CASE / switch necesitará agregar nuevos casos de vez en cuando, lo que significa que la base del código no es tan escalable y fácil de mantener como debería ser.
LSP permite que el código funcione más como el hardware:
No tiene que modificar su iPod porque compró un nuevo par de altavoces externos, ya que tanto los altavoces externos antiguos como los nuevos respetan la misma interfaz, son intercambiables sin que el iPod pierda la funcionalidad deseada.
fuente
typeof(someObject)
para decidir qué estás "permitido hacer", entonces seguro, pero ese es otro antipatrón por completo.para dar un ejemplo de la vida real con UndoManager de java
hereda de
AbstractUndoableEdit
cuyo contrato especifica que tiene 2 estados (deshacer y rehacer) y puede ir entre ellos con llamadas únicas aundo()
yredo()
sin embargo, UndoManager tiene más estados y actúa como un búfer de deshacer (cada llamada para deshacer
undo
algunas pero no todas las ediciones, debilitando la condición posterior)Esto lleva a la situación hipotética en la que agrega un UndoManager a un CompoundEdit antes de llamar y
end()
luego deshacer en ese CompoundEdit lo llevará a invocarundo()
en cada edición una vez que deje sus ediciones parcialmente deshacidasRodé el mío
UndoManager
para evitar eso (aunque probablemente debería cambiarle el nombreUndoBuffer
)fuente
Ejemplo: está trabajando con un marco de UI y crea su propio control de UI personalizado subclasificando la
Control
clase base. LaControl
clase base define un métodogetSubControls()
que debería devolver una colección de controles anidados (si los hay). Pero anula el método para devolver una lista de fechas de nacimiento de los presidentes de los Estados Unidos.Entonces, ¿qué puede salir mal con esto? Es obvio que la representación del control fallará, ya que no devuelve una lista de controles como se esperaba. Lo más probable es que la IU se bloquee. Está incumpliendo el contrato al que se espera que se adhieran las subclases de Control.
fuente
También puede verlo desde un punto de vista de modelado. Cuando dice que una instancia de clase
A
también es una instancia de claseB
, implica que "el comportamiento observable de una instancia de claseA
también puede clasificarse como comportamiento observable de una instancia de claseB
" (Esto solo es posible si la claseB
es menos específica que claseA
)Por lo tanto, violar el LSP significa que hay alguna contradicción en su diseño: está definiendo algunas categorías para sus objetos y luego no las está respetando en su implementación, algo debe estar mal.
Como hacer una caja con una etiqueta: "Esta caja contiene solo bolas azules", y luego arrojar una bola roja en ella. ¿De qué sirve una etiqueta de este tipo si muestra información incorrecta?
fuente
Recientemente heredé una base de código que tiene algunos infractores importantes de Liskov. En clases importantes. Esto me ha causado enormes cantidades de dolor. Déjame explicarte por qué.
Tengo
Class A
, que se deriva deClass B
.Class A
yClass B
comparte un montón de propiedades queClass A
anula con su propia implementación. Establecer u obtener unaClass A
propiedad tiene un efecto diferente al establecer u obtener exactamente la misma propiedadClass B
.Dejando a un lado el hecho de que esta es una forma absolutamente terrible de hacer la traducción en .NET, hay otros problemas con este código.
En este caso
Name
se usa como índice y variable de control de flujo en varios lugares. Las clases anteriores están plagadas en toda la base de código, tanto en su forma sin procesar como derivada. Violar el principio de sustitución de Liskov en este caso significa que necesito saber el contexto de cada llamada a cada una de las funciones que toman la clase base.El código usa objetos de ambos
Class A
yClass B
, por lo tanto, no puedo simplemente hacer unClass A
resumen para obligar a las personas a usarClass B
.Hay algunas funciones de utilidad muy útiles que funcionan
Class A
y otras funciones de utilidad muy útiles que funcionanClass B
. Idealmente me gustaría ser capaz de utilizar cualquier función de utilidad que puede operar enClass A
elClass B
. Muchas de las funciones que toman unaClass B
podrían tomar fácilmente una,Class A
si no fuera por la violación del LSP.Lo peor de esto es que este caso particular es realmente difícil de refactorizar ya que la aplicación completa depende de estas dos clases, opera en ambas clases todo el tiempo y se rompería de cien maneras si cambio esto (lo que voy a hacer de todas formas).
Lo que tendré que hacer para solucionar esto es crear una
NameTranslated
propiedad, que será laClass B
versión de laName
propiedad y cambiar muy cuidadosamente todas las referencias a laName
propiedad derivada para usar mi nuevaNameTranslated
propiedad. Sin embargo, si una de estas referencias se equivoca, toda la aplicación podría explotar.Dado que el código base no tiene pruebas unitarias a su alrededor, esto está bastante cerca de ser el escenario más peligroso que un desarrollador puede enfrentar. Si no cambio la infracción, tengo que gastar enormes cantidades de energía mental para realizar un seguimiento de qué tipo de objeto se está utilizando en cada método y si soluciono la infracción, podría hacer que todo el producto explote en un momento inoportuno.
fuente
BaseName
y tuvieraTranslatedName
acceso tanto al estilo de clase AName
como al significado de clase B? Luego, cualquier intento de accederName
a una variable de tipoB
sería rechazado con un error de compilación, por lo que puede asegurarse de que todas las referencias se conviertan en una de las otras formas.Si desea sentir el problema de violar LSP, piense qué sucede si solo tiene .dll / .jar de la clase base (sin código fuente) y tiene que construir una nueva clase derivada. Nunca puedes completar esta tarea.
fuente