Los subtipos deben poder sustituir a sus tipos base sin sorpresas
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.
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.
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.
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); } }
Misma familia de políticas sin herencia rígida: composición o políticas independientes que implementan el mismo contrato sin sorprender al llamador.
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.