Principio de Sustitución de Liskov
El Principio de Sustitución de Liskov, formulado por Barbara Liskov en 1988, es uno de los cinco principios SOLID de la programación orientada a objetos. Este principio establece que:
En términos más simples, este principio nos dice que las clases derivadas deben poder sustituir a sus clases base sin afectar la correctitud del programa.
El LSP
es fundamental para diseñar jerarquías de clases robustas y flexibles. Cuando se aplica correctamente, permite que el código que utiliza la clase base funcione correctamente con cualquier clase derivada, sin necesidad de conocer los detalles específicos de la implementación de la subclase.
Características clave del LSP
:
- Compatibilidad de firmas: Los métodos en la subclase deben tener firmas compatibles con los de la superclase.
- Precondiciones: Las precondiciones en la subclase no pueden ser más fuertes que en la superclase.
- Postcondiciones: Las postcondiciones en la subclase no pueden ser más débiles que en la superclase.
- Invariantes: Los invariantes de la superclase deben mantenerse en la subclase.
- Principio de sustitución: Las instancias de la subclase deben comportarse de manera consistente con las instancias de la superclase.
Ahora veamos cada uno de estos puntos.
1. Compatibilidad de firmas
Los métodos en la subclase deben tener firmas compatibles con los de la superclase. Esto significa que los métodos en la subclase deben tener el mismo nombre, tipos de parámetros y tipo de retorno (o un subtipo) que los métodos correspondientes en la superclase.
1class Forma {2 public double calcularArea() {3 return 0;4 }5}6
7class Circulo extends Forma {8 private double radio;9
10 public Circulo(double radio) {11 this.radio = radio;12 }13
14 @Override15 public double calcularArea() {16 return Math.PI * radio * radio;17 }18}19
20class Rectangulo extends Forma {21 private double ancho;22 private double alto;23
24 public Rectangulo(double ancho, double alto) {25 this.ancho = ancho;26 this.alto = alto;27 }28
29 @Override30 public double calcularArea() {31 return ancho * alto;32 }33}
Este ejemplo ilustra la compatibilidad de firmas. Tanto Circulo
como Rectangulo
extienden la clase Forma
y sobrescriben el método calcularArea()
. La firma del método (nombre, parámetros y tipo de retorno) se mantiene consistente en las subclases, mientras que la implementación específica varía según la forma. Esto permite que cualquier código que trabaje con Forma
pueda utilizar Circulo
o Rectangulo
sin problemas, demostrando la sustitución sin afectar la funcionalidad.
2. Precondiciones
Las precondiciones en la subclase no pueden ser más fuertes que en la superclase. Una precondición es una condición que debe ser verdadera antes de que un método se ejecute. La subclase puede debilitar las precondiciones, pero no fortalecerlas.
1class Vehiculo {2 protected int velocidad;3
4 public void acelerar(int incremento) {5 if (incremento > 0 && incremento <= 20) { // Precondición original6 System.out.println("Acelerando " + incremento + " km/h");7 velocidad += incremento;8 } else {9 throw new IllegalArgumentException("El incremento debe estar entre 1 y 20 km/h");10 }11 }12}13
14class VehiculoDeportivo extends Vehiculo {15 @Override16 public void acelerar(int incremento) {17 if (incremento > 0) { // Precondición debilitada18 System.out.println("Vehículo deportivo acelerando " + incremento + " km/h");19 velocidad += incremento;20 } else {21 throw new IllegalArgumentException("El incremento debe ser positivo");22 }23 }24}25
26public class EjemploPrecondiciones {27 public static void main(String[] args) {28 Vehiculo coche = new Vehiculo();29 VehiculoDeportivo cocheSport = new VehiculoDeportivo();30
31 coche.acelerar(15); // Funciona32 // coche.acelerar(25); // Lanza excepción33
34 cocheSport.acelerar(15); // Funciona35 cocheSport.acelerar(25); // Funciona, a diferencia de la superclase36 }37}
La clase Vehiculo
establece una precondición fuerte en su método acelerar()
: el incremento debe estar entre 1 y 20 km/h. La subclase VehiculoDeportivo
debilita esta precondición al permitir cualquier incremento positivo, sin límite superior.
Esta modificación es consistente con el LSP porque la subclase acepta un rango más amplio de valores de entrada que la superclase. Cualquier uso válido de Vehiculo
seguirá siendo válido con VehiculoDeportivo
, ya que este último puede manejar todos los casos que Vehiculo
maneja, y más. Esto asegura que VehiculoDeportivo
puede sustituir a Vehiculo en cualquier contexto sin causar errores inesperados, cumpliendo así con el principio de sustitución.
3. Postcondiciones
Las postcondiciones en la subclase no pueden ser más débiles que en la superclase. Una postcondición es una condición que debe ser verdadera después de que un método se ejecute. La subclase puede fortalecer las postcondiciones, pero no debilitarlas.
1class CajaFuerte {2 protected boolean estaAbierta;3 protected String contenido;4
5 public void abrir(String clave) {6 if ("1234".equals(clave)) {7 estaAbierta = true;8 System.out.println("Caja fuerte abierta");9 } else {10 System.out.println("Clave incorrecta");11 }12 }13
14 public String obtenerContenido() {15 if (estaAbierta) {16 return contenido;17 }18 return "La caja fuerte está cerrada";19 }20}21
22class CajaFuerteAvanzada extends CajaFuerte {23 private int intentosFallidos = 0;24
25 @Override26 public void abrir(String clave) {27 if ("1234".equals(clave)) {28 estaAbierta = true;29 intentosFallidos = 0;30 System.out.println("Caja fuerte avanzada abierta");31 } else {32 intentosFallidos++;33 if (intentosFallidos >= 3) {34 System.out.println("Caja fuerte bloqueada por seguridad");35 } else {36 System.out.println("Clave incorrecta");37 }38 }39 }40}
Este ejemplo ilustra el manejo de postcondiciones. La clase CajaFuerte
tiene la postcondición de que después de una apertura exitosa, estaAbierta
es verdadero. La subclase CajaFuerteAvanzada
mantiene esta postcondición y la fortalece al añadir una funcionalidad de seguridad adicional (bloqueo después de tres intentos fallidos). Esto cumple con el LSP porque la postcondición de la subclase es más fuerte: no solo garantiza que la caja se abra con la clave correcta, sino que también proporciona una capa adicional de seguridad.
4. Invariantes
Los invariantes de la superclase deben mantenerse en la subclase. Un invariante es una condición que siempre debe ser verdadera durante la vida de un objeto, antes y después de la ejecución de cualquier método.
1class ListaOrdenada {2 protected List<Integer> elementos = new ArrayList<>();3
4 public void agregar(int elemento) {5 elementos.add(elemento);6 Collections.sort(elementos);7 }8
9 public List<Integer> obtenerElementos() {10 return new ArrayList<>(elementos);11 }12}13
14class ListaOrdenadaDescendente extends ListaOrdenada {15 @Override16 public void agregar(int elemento) {17 elementos.add(elemento);18 elementos.sort(Collections.reverseOrder());19 }20}
Este ejemplo demuestra el mantenimiento de invariantes. La clase ListaOrdenada
tiene el invariante de que sus elementos siempre están ordenados de forma ascendente después de cada operación de agregado. La subclase ListaOrdenadaDescendente
mantiene un invariante similar, pero con un orden descendente.
Ambas clases preservan la propiedad fundamental de que la lista está ordenada después de cada operación, aunque el orden específico sea diferente. Esto respeta el LSP porque el invariante general de “lista ordenada” se mantiene, permitiendo que el código que espera una lista ordenada funcione correctamente con ambas clases.
Estos ejemplos ilustran cómo las subclases pueden extender o modificar el comportamiento de sus superclases mientras mantienen la consistencia con el Principio de Sustitución de Liskov, asegurando que puedan ser utilizadas de manera intercambiable sin causar errores o comportamientos inesperados en el programa.
5. Principio de sustitución
Las instancias de la subclase deben comportarse de manera consistente con las instancias de la superclase. Esto significa que cualquier código que funcione con una instancia de la superclase debe funcionar igualmente bien con una instancia de la subclase, sin necesidad de conocer la diferencia.
Ejemplo:
1class Ave {2 public void volar() {3 System.out.println("El ave está volando");4 }5}6
7class Pinguino extends Ave {8 @Override9 public void volar() {10 throw new UnsupportedOperationException("Los pingüinos no pueden volar");11 }12}13
14public class Ejemplo {15 public static void hacerVolarAve(Ave ave) {16 ave.volar();17 }18
19 public static void main(String[] args) {20 Ave golondrina = new Ave();21 Ave pinguino = new Pinguino();22
23 hacerVolarAve(golondrina); // Funciona bien24 hacerVolarAve(pinguino); // Lanza una excepción, violando el LSP25 }26}
Este ejemplo viola el LSP porque Pinguino
no puede ser sustituido por Ave
sin cambiar el comportamiento del programa. Una mejor solución sería rediseñar la jerarquía para que Pinguino
no herede de una clase que asume la capacidad de volar.