El Principio de Sustitución de Liskov (LSP, Liskov Substitution Principle) establece que si S
es un subtipo de T
, entonces los objetos de tipo T
pueden ser reemplazados por objetos de tipo S
sin alterar el comportamiento correcto del programa. Fue propuesto por Barbara Liskov y Jeannette Wing en 1987.
En términos prácticos, LSP implica que las subclases deben cumplir las expectativas que el código cliente tiene sobre la clase base. Si una subclase rompe invariantes, precondiciones o postcondiciones, la jerarquía se vuelve frágil.
Para mantener la sustitución válida, una subclase debe:
UnsupportedOperationException
.Consideremos la jerarquía habitual donde Cuadrado
hereda de Rectangulo
. A primera vista parece correcto, pero al utilizarlo en un algoritmo genérico encontramos problemas.
class Rectangulo {
private int ancho;
private int alto;
void setAncho(int ancho) { this.ancho = ancho; }
void setAlto(int alto) { this.alto = alto; }
int area() { return ancho * alto; }
}
class Cuadrado extends Rectangulo {
@Override
void setAncho(int lado) {
super.setAncho(lado);
super.setAlto(lado);
}
@Override
void setAlto(int lado) {
super.setAncho(lado);
super.setAlto(lado);
}
}
Un algoritmo que espera modificar ancho y alto por separado deja de funcionar correctamente cuando recibe un cuadrado. El contrato implícito de Rectangulo
se rompe.
Podemos eliminar la herencia y modelar ambos conceptos mediante interfaces o composición. Aquí usamos una interfaz común para figuras de cuatro lados y delegamos la creación en clases distintas.
interface Figura {
int area();
}
class Rectangulo implements Figura {
private final int ancho;
private final int alto;
Rectangulo(int ancho, int alto) {
this.ancho = ancho;
this.alto = alto;
}
public int area() {
return ancho * alto;
}
}
class Cuadrado implements Figura {
private final int lado;
Cuadrado(int lado) {
this.lado = lado;
}
public int area() {
return lado * lado;
}
}
Al utilizar composición en lugar de herencia forzada, cada clase respeta su contrato y se puede reemplazar libremente entre objetos que esperan una Figura
.
Supongamos un módulo bancario que procesa transferencias. Una implementación incorrecta podría reutilizar la clase base pero cambiar restricciones de manera sorpresiva.
class Transferencia {
void ejecutar(BigDecimal monto) {
if (monto.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Monto inválido");
}
// lógica común
}
}
class TransferenciaInternacional extends Transferencia {
@Override
void ejecutar(BigDecimal monto) {
if (monto.compareTo(new BigDecimal("1000")) < 0) {
throw new IllegalArgumentException("Monto mínimo 1000 para internacionales");
}
super.ejecutar(monto);
}
}
El cliente que trabaja con Transferencia
espera que cualquier monto positivo sea válido, pero la subclase viola esa expectativa ampliando las precondiciones. Esto rompe LSP.
En lugar de heredar, definimos una interfaz y separamos las validaciones para cada caso.
interface OperacionTransferencia {
void ejecutar(BigDecimal monto);
}
class TransferenciaLocal implements OperacionTransferencia {
public void ejecutar(BigDecimal monto) {
validarMontoPositivo(monto);
// lógica local
}
private void validarMontoPositivo(BigDecimal monto) {
if (monto.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Monto inválido");
}
}
}
class TransferenciaInternacional implements OperacionTransferencia {
public void ejecutar(BigDecimal monto) {
validarMontoPositivo(monto);
validarMinimoInternacional(monto);
// lógica internacional
}
private void validarMontoPositivo(BigDecimal monto) {
if (monto.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Monto inválido");
}
}
private void validarMinimoInternacional(BigDecimal monto) {
if (monto.compareTo(new BigDecimal("1000")) < 0) {
throw new IllegalArgumentException("Monto mínimo 1000 para internacionales");
}
}
}
Ahora el cliente depende de la interfaz OperacionTransferencia
y cada implementación asume su propio conjunto de reglas sin violar las expectativas de las otras.
El Principio de Sustitución de Liskov fortalece el polimorfismo y prepara el terreno para aplicar correctamente la segregación de interfaces y la inversión de dependencias. En los siguientes capítulos veremos cómo estos principios continúan construyendo estructuras robustas.