Si ya leíste el principio Open/Closed, hoy vamos a hablar del principio de sustitución de Liskov:
- Principio de Responsabilidad Única
- Principio Open/Closed
- Principio de Sustitución de Liskov
- Principio de Segregación de Interfaces
- Principio de Inversión de Dependencias
Principio de Sustitución de Liskov
El principio de sustitución de Liskov nos dice que si en alguna parte de nuestro código estamos usando una clase, y esta clase es extendida, tenemos que poder utilizar cualquiera de las clases hijas y que el programa siga siendo válido. Esto nos obliga a asegurarnos de que cuando extendemos una clase no estamos alterando el comportamiento de la padre.
Este principio viene a desmentir la idea preconcebida de que las clases son una forma directa de modelar la realidad. Esto no siempre es así, y el ejemplo más típico es el de un rectángulo y un cuadrado. En breve veremos por qué.
La primera en hablar de él fue Bárbara Liskov (de ahí el nombre), una reconocida ingeniera de software americana.
¿Cómo detectar que estamos violando el principio de sustitución de Liskov?
Seguro que te has encontrado con esta situación muchas veces: creas una clase que extiende de otra, pero de repente uno de los métodos te sobra, y no sabes que hacer con él. Las opciones más rápidas son bien dejarlo vacío, bien lanzar una excepción cuando se use, asegurándote de que nadie llama incorrectamente a un método que no se puede utilizar. Si un método sobrescrito no hace nada o lanza una excepción, es muy probable que estés violando el principio de sustitución de Liskov. Si tu código estaba usando un método que para algunas concreciones ahora lanza una excepción, ¿cómo puedes estar seguro de que todo sigue funcionando?

Imagen de Derick Balley
Otra herramienta que te avisará fácilmente son los tests. Si los tests de la clase padre no funcionan para la hija, también estarás violando este principio. Con este segundo caso vamos a ver el ejemplo.
Ejemplo
En la vida real tenemos claro que un cuadrado es un rectángulo con los dos lados iguales. Si intentamos modelar un cuadrado como una concreción de un rectángulo, vamos a tener problemas con este principio:
[java]
public class Rectangle {
private int width;
private int height;
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int calculateArea() {
return width * height;
}
}
[/java]
Y un test que comprueba el área:
[java]
@Test
public void testArea() {
Rectangle r = new Rectangle();
r.setWidth(5);
r.setHeight(4);
assertEquals(20, r.calculateArea());
}
[/java]
La definición del cuadrado sería la siguiente:
[java]
public class Square extends Rectangle {
@Override public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override public void setHeight(int height) {
super.setHeight(height);
super.setWidth(height);
}
}
[/java]
Prueba ahora en el test a cambiar el rectángulo por un cuadrado. ¿Qué ocurrirá? Este test no se cumple, el resultado sería 16 en lugar de 20. Estamos por tanto violando el principio de sustitución de Liskov.
¿Cómo lo solucionamos?
Hay varias posibilidades en función del caso en el que nos encontremos. Lo más habitual será ampliar esa jerarquía de clases. Podemos extraer a otra clase padre las características comunes y hacer que la antigua clase padre y su hija hereden de ella. Al final lo más probable es que la clase tenga tan poco código que acabes teniendo un simple interfaz. Esto no supone ningún problema en absoluto:
<
pre class=””>
[java]
public interface IRectangle {
int getWidth();
int getHeight();
int calculateArea();
}
public class Rectangle implements IRectangle {
…
}
public class Square implements IRectangle {
…
}
[/java]
Pero para este caso en particular, nos encontramos con una solución mucho más sencilla. La razón por la que no se cumple que un cuadrado sea un rectángulo, es porque estamos dando la opción de modificar el ancho y alto después de la creación del objeto. Podemos solventar esta situación simplemente usando inmutabilidad.
La inmutabilidad es un tema muy interesante que trataré en algún artículo más adelante. Consiste en que una vez que se ha creado un objeto, el estado del mismo no puede volver a modificarse. La inmutabilidad tiene múltiples ventajas, entre ellas un mejor uso de memoria (todo su estado es final) o seguridad en múltiples hilos de ejecución. Pero ciñéndonos al ejemplo, ¿cómo nos ayuda aquí la inmutabilidad? De esta forma:
[java]
public class Rectangle {
public final int width;
public final int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
}
public class Square extends Rectangle {
public Square(int side) {
super(side, side);
}
}
[/java]
Desde el momento de la instanciación del objeto, todo lo que hagamos con él será válido, ya usemos un rectángulo o un cuadrado. El problema que había detrás de este ejemplo es que la asignación de una parte del estado modificaba mágicamente otro campo. Sin embargo, con este nuevo enfoque, al no permitir las modificaciones, el funcionamiento de ambas clases es totalmente predecible.
Conclusión
El principio de Liskov nos ayuda a utilizar la herencia de forma correcta, y a tener mucho más cuidado a la hora de extender clases. En la práctica nos ahorrará muchos errores derivados de nuestro afán por modelar lo que vemos en la vida real en clases siguiendo la misma lógica. No siempre hay una modelización exacta, por lo que este principio nos ayudará a descubrir la mejor forma de hacerlo.
La cuarta parte tratará sobre el principio de segregación de interfaces. ¿Qué te ha parecido hasta ahora? ¿Crees que tiene sentido aplicar estos principios en tu día a día?
Author: Antonio Leiva
Soy un apasionado de Kotlin. Hace ya más de dos años que estudio el lenguaje y su aplicación a Android para ayudarte a ti a aprenderlo de la forma más sencilla posible.
Buenas
Me quedó un tanto abstracto este principio… algun otro ejemplo aplicado?
Imagina que tienes una clase `Animal`, que tiene un método `correr()`, y creas la clase `Tortuga` extendiendo de ella, sobrescribes el método lanzando una excepción “Las tortugas no pueden correr”. Esto viola el principio de sustitución de Liskov, porque si en tu código estabas usando un objeto `Animal animal = new Animal()`, y ahora en vez de eso haces `Animal animal = new Tortuga()`, el programa estará roto en cuanto se llame el método `correr()`.
¿Te queda más claro con este ejemplo?
con este ejemplo entendí mejor , gracias y buen trabajo a hora es empezar a aplicarlo
jajajja Andrés tiene razón, me fue mejor con este ejemplo sobre la tortuga
Antonio, primero, Muchas gracias por tomarte el trabajo de explicar estos conceptos digamoslo un poco abstractos… Estoy estudiandolos… en el ejemplo que das de la tortuga, bastaría tambien para no infringir este principio el quitar el metodo correr() de la clase abstracta padre, ya que no todos los animales corren? Más bien ponerlo en una interfaz?
Estoy esperando a llegar al ID (Inversión de dependencias) es el que más me cuesta ya que aún no entiendo todas las funcionalidades de las interfaces aplicadas a patrones y buenas prácticas, en todos los sitios solo te las explican como una alternativa a la herencia multiple y como una alternativa solo para implementar metodos que se necesitan, pero no esa funcionalidad de mmm instanciarlas a través de clases que las implementen y así aplicar por ej patrones de diseño o principios como SOLID.
Sí, exacto. No está bien dar por hecho que todos los animales corren.
Gracias Antonio, Esta ha sido una de las explicaciones de Liskov más simples y claras que he leído. Chapeau.
Muchas gracias, la verdad es que son conceptos difíciles de poner en palabras, y lleva un rato organizarlo y saber como expresarlo. Me alegra saber que cumple su objetivo.
Antonio, en el ultimo ejemplo de Square, el test tampoco funcionaria, por lo tanto no se cumpliría el principio de Liskov.
El test anterior no valdría para el último código. Puesto que ya no existen setters, no tiene sentido un test que valide qué ocurre cuando se setean valores. Sin embargo, cualquier código que use Rectangle, ahora puede usar Square sin temor a que se modifique el comportamiento. En mi opinión sí que se cumple Liskov, pero igual se me está escapando algo.
Creo que para entender el concepto que es el objetivo del post está más que bien explicado.
El test no se cumple por que se está comparando con un valor fijo dentro del propio test, y eso puede inducir a una mala interpretación del concepto.
Un objeto de la clase Square requiere un solo parámetro en el constructor y Rectangle requiere dos. Entonces, ¿como se cumple el principio de Liskov?
Estoy de acuerdo con lo que dice Manuel Calero Solís, pero tu respondes que “Sin embargo, cualquier código que use Rectangle, ahora puede usar Square sin temor a que se modifique el comportamiento” sin embargo esto no es cierto debido a que la hacer la instancia los constructores de ambos objetos son distintos ocasionando un error al querer intercambiar clases.
Lo importante es la interfaz del objeto, y no su implementación, ya que en general trabajarás con la abstracción. En cualquier caso, aquí tienes una conversación sobre LSP y los constructores: https://softwareengineering.stackexchange.com/questions/302476/liskov-principle-with-different-constructor-parameters
Habría que introducir una nueva clase entonces que fuera AnimalQueCorre que hereda de Animal. Tortuga heredaría de Animal pero no de AnimalQueCorre. Bien explicado.
Me llama la atención lo de evitar métodos que no devuelvan nada. Con lo de lanzar la excepción queda más claro. El problema es que es algo muy común en el mantenimiento de grandes aplicaciones. Tienes 200 tablas, todo eran gacelas, tigres o guepardos… de repente un día hay un mantenimiento y te meten una tortuga.
Dilema: ¿Cambio el código en 300 sitios en el gran monolito éste que es la app o Tortuga.Corre() lo convierto en un método vacío?
Venga a ver quién es el guapo que se arriesga. Aquí es donde SOLID queda más en el ámbito académico. Tortuga corre! Y tortuga pasa de ti y sigue andando, lo aceptas y tira millas.
El tema es que para poder refactorizar un código grande necesitas una batería de pruebas grande, que pruebe que tus cambios no van a romper todo lo demás. Yo no creo que SOLID sea algo académico, sólo que no es fácil encontrar el equilibrio entre volverse loco aplicándolo y no aplicarlo nada.
El tema de la orientación a objetos no tiene nada que ver con las “tablas”. La orientación a objetos y el modelo relacional son dos modelos diferentes y no puedes pasar de uno a otro.
Y por otro lado, usar mejores abstracciones solucionarían el problema que comentas:
En lugar de “AnimalQueCorre” que herede de “Animal” podriamos tener “Animal” con método “mover” y usar la composición en lugar de la herencia para este tema. Así la jerarquía de animales quedaría más limpia.
public class Animal {
private MotorLocomocion motorLocomocion;
public Animal(MotorLocomocion motorLocomocion) {
this.motorLocomocion = motorLocomocion;
}
public void mover() {
motorLocomocion.mover();
}
}
La interfaz de MotorLocomocion tendría varias implementaciones: volar, correr, nadar.
No sé si me explico.
No veo clara esa solución. Podría haber algún animal que pudiese moverse de varias maneras: una ser humano, por ejemplo, puede correr y nadar (bueno, vale, depende de qué persona, pero, para el ejemplo, imagínate a un acuatleta).
Siempre puedes implementar los métodos de locomoción a base de interfaces, o asignar distintos métodos mediante el patrón Decorator que se comentó en el principio open/closed no?
El error esta en el ejemplo en sí: por contraintuitivo que parezca, un cuadrado no es un subtipo de un rectángulo. El criterio de especialización para el diseño de subclases no implica que cualquier especialización que se nos ocurra sea legítima. Este es un feo, y popular incluso en la universidad, ejemplo
Yo sí creo que el ejemplo es bueno, primero porque en la vida real un cuadrado sí que es un rectángulo con sus lados iguales, y al aplicar orientación a objetos tendemos (a veces equivocadamente) a modelar directamente la realidad. Por tanto, este caso (y similares) se puede dar con bastante facilidad en el código de cualquier programa, y Liskov nos ayuda a detectar que igual no es buena idea esta modelización. Por otro lado, en el caso de que sean inmutables, sí que se cumple que un cuadrado sea una especialización de un rectángulo.
De acuerdo. La definición de subtipo está dada por el Principio de Sustitución. El problema parte de la idea equivocada de copiar la realidad en el software, donde parece que la herencia es el idea taxonómico del software. Y los apaños de inmutabilidad u otros son eso: apaños. El software es un universo con sus propiedades y el mundo exterior es distinto.
Considero que el error esta en el enfoque del ejemplo. En el test del rectángulo se utilizan las condiciones iniciales con valores de 4 y 5, mientras que al Cuadrado lo haces con valores de 4 por lado. Es claro que los resultados van a ser diferentes porque las condiciones iniciales son diferentes. Si probamos con un Rectángulo de lados 10 y 20 también va a dar un valor diferente, y no quiere decir que la clase tenga errores de abstracción. Quizás sea un ejemplo mas claro al comparar Rectágulos con Prismas (con una dimensión mas). Igualmente las explicaciones son muy claras. Te felicito y agradezco, logro despejarme algunas dudas.
Al final lo más probable es que la clase tenga tan poco código QUE acabes teniendo un simple interfaz.
Falta ese “que”, excelente artículo obviando esa minucia.
Cambiado, muchas gracias!
Si he entendido bien la idea de este principio, es que si en la clase base (por ejemplo Animal) hay definido un método con una funcionalidad especifica en esa clase, si un subtipo de esa clase sobrescribe (override) ese método y cambia su funcionalidad (aplicando polimorfismo) a algo que se adapte más a su subtipo y cambiando la funcionalidad que hace el método de la clase base, al intentar cambiar el tipo base por el subtipo no funcionaria de la misma manera y se violaría este principio.
Esto sería muy difícil de cumplir ya que si tenemos una clase base y los subtipos tienen los método de la clase base y además métodos suyos propios que necesitan para funcionar al intentar crear un objeto desde la clase base no tendría acceso a esos métodos teniendo que hacer un casting para saber de qué subtipo son, esto también estaría incumpliendo el principio ya que la clase base no puede acceder a esos métodos porque no los conoce y no se podría sustituir.
¿Como se haría el diseño para tener una clase base con algunos métodos que funcionen por igual en las clases subtipos y esas clases subtipos tengan sus propios métodos que no están en la clase base? Porque de esta manera tendrías que saber de que subtipo se trata y hacer un casting a ese tipo y esto incumpliría el principio open/close.
Muchas gracias por el ejemplo y la explicacion fueron muy utiles! Saludos querido!
Buena la explicación, utilice el ejercicio para conversar con mi equipo de trabajo, sin embargo te recomiendo ajustar un pequeño detalle:
public interface IRectangle {
int getWidth();
int getHeight();
int calculateArea();
}
public class Rectangle extends IRectangle {
…
}
public class Square extends IRectangle {
…
}
En el ejercicio extiendes de la interface, y como sabes, no se extiendes sino que se implementan.
Saludos.
Muy cierto, muchas gracias por avisar!
Porque mencionas esto , acaso Dios no es el mejor de los programadores y nosotros somos los que no le entendemos ?
“Este principio viene a desmentir la idea preconcebida de que las clases son una forma directa de modelar la realidad”. Esto no siempre es así, y el ejemplo más típico es el de un rectángulo y un cuadrado.
En total desacuerdo.Quien observa la realidad aprende a programar mejor.
Nadie dice que no, pero lo que es cierto es que la Programación Orientada a Objetos no encaja exactamente con nuestras ideas preconcebidas de la realidad. Así que no siemper hay que hacer modelos exactos, sino pensar más en cómo eso va a ayudar a que mis modelos puedan evolucionar mejor en el tiempo. Una cosa es la naturaleza y otra cómo nos comunicamos con la máquina, así que en términos más teológicos no me meto 😄
Muchas gracias, muy clasificadoras tus explicaciones, me encanta el ejemplo de la tortuga!
Gracia Xavi!
Con última construcción de clases se viola el principio de liskov. Humildemente creo que no solo es importarte la implementación de cada una de las clases, tanto de la padre como la hija porque, en este caso, no podría sustituir la clase padre por la clase hija ya que no tienen el mismo comportamiento y los resultados no son iguales. Si tienes :
Rectangle r= new Rectangle(5,4);
r.calculateArea();
El resultado sería 20, pero si quiero comprobar el principio, entonces sustituiría de esta manera:
Square r=new Square(5)
r.calculateArea(); El resultado sería 25 y lo que quiero es poder pasarle dos parámetros para que se siga comportando como un rectángulo y no como un cuadrado. Se viola el principio, no puedo sustituir la clase padre por la hija