El Principio de Sustitución de Liskov exige que una clase hija pueda sustituir a su clase padre sin romper el programa.
Llegamos al ecuador de SOLID con la letra L: el Principio de Sustitución de Liskov (Liskov Substitution Principle o LSP).
Este principio lleva el nombre de Barbara Liskov, ganadora del premio Turing. Se basa en una idea que planteó en 1987 y que más tarde formalizó junto a Jeannette Wing.
Su definición formal asusta un poco:
“Si S es un subtipo de T, entonces los objetos de tipo T pueden ser sustituidos por objetos de tipo S sin alterar las propiedades deseables del programa.”
¡Uf! ¿Qué significa esto en castellano? Básicamente, si una clase hereda de otra, la clase hija debe respetar el comportamiento esperado cuando se use en lugar de la clase padre.
El problema clásico: el cuadrado y el rectángulo
Para entender esto, no hay mejor ejemplo que el famoso problema del cuadrado y el rectángulo. Es el ejemplo canónico porque demuestra cómo la lógica matemática y la lógica de objetos no siempre coinciden.
En geometría, un cuadrado es un rectángulo (un caso especial donde todos los lados son iguales). Así que nuestra intuición nos dice: “Genial, hago que Cuadrado herede de Rectángulo”.
La clase padre: Rectangulo
Tenemos un rectángulo normal y corriente. Asumimos que podemos cambiar el ancho y el alto de forma independiente.
public class Rectangulo
{
public virtual int Ancho { get; set; }
public virtual int Alto { get; set; }
public int CalcularArea() => Ancho * Alto;
}
La clase hija: Cuadrado
Como un cuadrado debe tener lados iguales, sobrescribimos los setters para forzar esa regla.
public class Cuadrado : Rectangulo
{
public override int Ancho
{
set { base.Ancho = value; base.Alto = value; }
}
public override int Alto
{
set { base.Ancho = value; base.Alto = value; }
}
}
La prueba que falla
En este punto se rompe el LSP. Supongamos una función que recibe un Rectangulo (la clase padre) y hace una prueba.
public void TestArea(Rectangulo r)
{
r.Ancho = 5;
r.Alto = 4;
// Si es un rectángulo, el área debería ser 5 * 4 = 20.
if (r.CalcularArea() != 20)
throw new Exception("¡Matemáticas rotas!");
}
Si pasamos un objeto Cuadrado a esta función (que espera un Rectangulo), el resultado no es el esperado:
r.Ancho = 5→ El cuadrado se pone a 5×5.r.Alto = 4→ El cuadrado se pone a 4×4 (porque al asignar el alto también cambiamos el ancho).CalcularArea()devuelve 16.- ¡La prueba falla! El programa esperaba 20.
Conclusión: Cuadrado no es sustituible por Rectangulo. Hemos violado el LSP.
Otro síntoma: excepciones no esperadas
Otro caso muy común de violación del LSP es cuando forzamos una interfaz y “desactivamos” partes que no nos sirven.
Supongamos una clase base Ave con el método Volar().
Creamos la clase Pinguino que hereda de Ave. Como los pingüinos no vuelan, hacemos esto:
public class Pinguino : Ave
{
public override void Volar()
{
throw new NotImplementedException("Los pingüinos no vuelan");
}
}
Si tienes un código que recorre una lista de Ave y les dice a todas Volar(), cuando llegue al pingüino el programa explotará.
Si tienes que lanzar una excepción, dejar un método vacío o verificar el tipo con if (obj is Pinguino) para evitar llamar a un método, estás violando el LSP.
Las reglas del contrato
El LSP se basa en el “Diseño por Contrato”. Cuando heredas, aceptas un contrato.
- Precondiciones: No puedes ser más exigente que tu padre. (Si tu padre acepta cualquier número, tú no puedes aceptar solo positivos).
- Postcondiciones: No puedes ofrecer menos que tu padre. (Si tu padre promete devolver un objeto válido, tú no puedes devolver
null). - Invariantes: Debes mantener las reglas que el padre asume como ciertas siempre (como en el caso del rectángulo, donde se asume que alto y ancho son independientes).
¿Cómo solucionarlo?
Generalmente, las violaciones del LSP indican que nuestra jerarquía de herencia está mal planteada.
En el caso del Ave:
- No todas las aves vuelan. Quizás
Volar()no debería estar enAve. - Podríamos tener
Avey luegoAveVoladorayAveNoVoladora.
En el caso del rectángulo:
- Un Cuadrado y un Rectángulo son figuras, pero no comparten comportamiento de modificación.
- Podrían implementar una interfaz común
IFiguraque tengaCalcularArea(), pero no heredar uno del otro.
Pregúntate siempre: ¿Es un…? (Is-a). No en el sentido del mundo real, sino en el del comportamiento dentro de tu programa.