← dev-notes
SOLID · 03 / 05

LSP
LISKOV SUBSTITUTION

Los subtipos deben poder sustituir a sus tipos base sin sorpresas

Contratos Pre / postcondiciones Invariantes
01 Definición formal
FORMAL · DEFINICIÓN

Si S es un subtipo de T, los objetos de tipo T pueden sustituirse por objetos de tipo S sin alterar las propiedades deseables del programa (corrección frente a la especificación). Las subclases deben honrar el contrato del tipo base: no endurecer precondiciones, no debilitar postcondiciones ni romper invariantes que el cliente espera.

En la práctica: si tu código trabaja con CustomerDiscountPolicy, cualquier subclase debe aplicar descuentos sin exigir condiciones nuevas ni fallar en escenarios donde la clase base funcionaba.

02 Idea clave

La herencia no es solo «un premium es un cliente»: el sustituto debe comportarse como el tipo declarado. Si el checkout llama a policy.apply(cart) y una subclase lanza excepción por reglas que el cliente no conocía, el programa que era correcto con la base deja de serlo con el subtipo.

03 Cuando se rompe · Java

Política «Premium» que rechaza carritos con artículos en oferta: refuerza precondiciones respecto a la política estándar que acepta cualquier carrito no vacío.

ROMPE LSP — SUBTIPO MÁS EXIGENTE
public class StandardDiscountPolicy {
    public Money apply(Cart cart) {
        if (cart.isEmpty()) {
            throw new IllegalArgumentException("carrito vacío");
        }
        return cart.total().multiplyPercent(5);
    }
}

public final class PremiumDiscountPolicy extends StandardDiscountPolicy {
    @Override
    public Money apply(Cart cart) {
        if (cart.hasPromoItems()) {
            // el cliente que solo conocía StandardDiscountPolicy
            // no esperaba este rechazo → sustitución insegura
            throw new IllegalStateException("premium no aplica con promos");
        }
        return cart.total().multiplyPercent(12);
    }
}
04 Aplicación adecuada · Java

Misma familia de políticas sin herencia rígida: composición o políticas independientes que implementan el mismo contrato sin sorprender al llamador.

RESPETA LSP — CONTRATO COMÚN, SIN SORPRESAS
public interface DiscountPolicy {
    Money apply(Cart cart);
}

public final class StandardDiscount implements DiscountPolicy {
    @Override public Money apply(Cart cart) {
        if (cart.isEmpty()) throw new IllegalArgumentException("carrito vacío");
        return cart.total().multiplyPercent(5);
    }
}

public final class PremiumDiscount implements DiscountPolicy {
    @Override public Money apply(Cart cart) {
        if (cart.isEmpty()) throw new IllegalArgumentException("carrito vacío");
        // sin promos: misma precondición que el estándar
        Money base = cart.totalExcludingPromos();
        return base.multiplyPercent(12);
    }
}

El checkout solo conoce DiscountPolicy; «premium» no invalida carritos con promos de forma explosiva: o bien ignora esas líneas en el cálculo, o bien devuelve Money.ZERO con reglas documentadas — pero no exige al cliente ramas especiales por tipo concreto.

05 Relación con patrones
PUENTE SOLID ↔ GOF
  • Strategy/State: suelen reemplazar herencia frágil por comportamiento intercambiable con contrato claro.
  • Template Method: úsalo solo si las subclases pueden respetar invariantes del algoritmo base.
  • Evita sobreingeniería: si un subtipo rompe contrato, prioriza composición antes que “forzar” herencia.