El principio de sustitución de Liskov, conocido como LSP, es uno de los pilares fundamentales dentro de la metodología SOLID en programación orientada a objetos. Este principio, nombrado en honor a Bárbara Liskov, nos indica que las subclases deben mantener las propiedades y condiciones que establece la clase base, de modo que podamos sustituir una instancia de la clase base por una de sus subclases sin que el comportamiento del programa se vea afectado.
Para entenderlo mejor, pensemos en una jerarquía de clases que representa una cuenta bancaria. Imaginemos que tenemos una interfaz llamada CuentaBancaria con métodos como depositar y extraer. El contrato que define esta interfaz incluye ciertas condiciones: por ejemplo, no tiene sentido permitir extraer más dinero del que hay en la cuenta, o no aceptar depósitos con valores negativos. Además, puede haber reglas adicionales, como limitar la cantidad máxima que se puede depositar en un día para evitar fraudes, o registrar cada transacción en un historial interno.
Cuando implementamos esta interfaz en subclases como CuentaBasica o CuentaNomina, estas deben respetar todas esas condiciones. No podemos permitir que una subclase ignore las reglas, como permitir extraer más dinero del disponible. Si garantizamos que todas las subclases cumplen con el contrato de la clase base, entonces estamos aplicando correctamente el principio de sustitución de Liskov.
El nombre del principio proviene de la idea de sustitución: si tenemos métodos o clases que trabajan con la clase base, deberían funcionar igual de bien si en lugar de una instancia de la clase base reciben una instancia de cualquier subclase. Por ejemplo, un método que opera con CuentaBancaria no debería importar si la cuenta es una CuentaBasica o una CuentaNomina, siempre que ambas cumplan con el contrato. Esto evita que el programa se rompa o se comporte de forma inesperada al cambiar el tipo concreto de la cuenta.
Sin embargo, en la práctica, a veces nos encontramos con subclases que no cumplen este principio. Por ejemplo, imaginemos una subclase llamada CuentaDeposito, que representa un depósito a plazo fijo donde no se puede extraer dinero hasta que vence el plazo. En este caso, el método extraer no puede funcionar como en la clase base, porque la operación no está permitida. Esto rompe el principio de sustitución, ya que no podemos usar una CuentaDeposito en un contexto donde se espera una cuenta que permita extracciones.
Este tipo de situaciones nos invita a reflexionar sobre el diseño de nuestras clases base. Quizás la interfaz CuentaBancaria debería estar dividida en varias interfaces más específicas, por ejemplo, una para cuentas que permiten extracciones y otra para cuentas que solo permiten depósitos. Así, evitamos que subclases tengan que implementar métodos que no tienen sentido para ellas, y mantenemos el cumplimiento del principio de sustitución.
Otro indicio de que no estamos respetando este principio es cuando en nuestro código tenemos que hacer comprobaciones constantes del tipo concreto de la cuenta, usando instanceof o mecanismos similares. Si nos vemos preguntando repetidamente si una cuenta es de tipo A, B o C para decidir qué hacer, probablemente nuestro diseño no está bien abstraído. En lugar de eso, deberíamos apoyarnos en interfaces intermedias que reflejen las capacidades reales de cada tipo de cuenta, permitiendo que el código trabaje con la abstracción adecuada sin necesidad de conocer detalles concretos.
En definitiva, el principio de sustitución de Liskov nos guía a diseñar jerarquías de clases y contratos claros que aseguren que las subclases puedan reemplazar a sus clases base sin alterar el comportamiento esperado. Esto facilita la extensibilidad, el mantenimiento y la robustez de nuestro código orientado a objetos.