El Principio Abierto/Cerrado (OCP, Open/Closed Principle) afirma que el código debe estar abierto a la extensión pero cerrado a la modificación. Fue formulado por Bertrand Meyer y se convirtió en una piedra angular del diseño orientado a objetos moderno.
Aplicar OCP significa que nuevas funcionalidades se agregan mediante componentes adicionales que colaboran con los existentes, evitando alterar clases ya probadas. Esto disminuye el riesgo de introducir errores y facilita la evolución de sistemas complejos.
Los requerimientos cambian constantemente: surgen nuevas reglas, se agregan integraciones y se modifican procesos internos. Si cada cambio obliga a editar clases que ya funcionan, el proyecto se vuelve frágil. OCP propone encapsular variaciones en extensiones controladas.
OCP se apoya en DIP e interfaces bien definidas. Para extender sin modificar, el código debe depender de abstracciones estables. A su vez, SRP facilita la extensión porque cada clase cumple un rol específico.
if
o switch
que crecen con cada nueva variante.Analicemos un servicio que calcula comisiones según el tipo de vendedor. Cada vez que la compañía agrega una categoría nueva, hay que abrir la clase y modificarla.
class CalculadoraComisiones {
double calcular(Vendedor vendedor) {
if (vendedor.tipo() == TipoVendedor.JUNIOR) {
return vendedor.ventas() * 0.02;
}
if (vendedor.tipo() == TipoVendedor.SENIOR) {
return vendedor.ventas() * 0.035;
}
if (vendedor.tipo() == TipoVendedor.PARTNER) {
return vendedor.ventas() * 0.05;
}
throw new IllegalArgumentException("Tipo de vendedor no soportado");
}
}
El código es difícil de mantener: agregar un nuevo tipo implica editar la clase, compilar nuevamente y revalidar todo el flujo.
Para cumplir OCP, se define una abstracción para el cálculo de comisiones y se delega la lógica en implementaciones intercambiables. El servicio principal confía en la abstracción y se mantiene cerrado a cambios.
interface PoliticaComision {
boolean aplicaA(Vendedor vendedor);
double calcular(Vendedor vendedor);
}
class CalculadoraComisiones {
private final List<PoliticaComision> politicas;
CalculadoraComisiones(List<PoliticaComision> politicas) {
this.politicas = politicas;
}
double calcular(Vendedor vendedor) {
return politicas.stream()
.filter(politica -> politica.aplicaA(vendedor))
.findFirst()
.map(politica -> politica.calcular(vendedor))
.orElseThrow(() -> new IllegalArgumentException("Tipo no soportado"));
}
}
Ahora los nuevos tipos se agregan implementando PoliticaComision
sin modificar CalculadoraComisiones
.
class ComisionJunior implements PoliticaComision {
public boolean aplicaA(Vendedor vendedor) {
return vendedor.tipo() == TipoVendedor.JUNIOR;
}
public double calcular(Vendedor vendedor) {
return vendedor.ventas() * 0.02;
}
}
class ComisionSenior implements PoliticaComision {
public boolean aplicaA(Vendedor vendedor) {
return vendedor.tipo() == TipoVendedor.SENIOR;
}
public double calcular(Vendedor vendedor) {
return vendedor.ventas() * 0.035;
}
}
Al registrar una nueva política en la lista (por ejemplo, para vendedores PARTNER
), la calculadora queda extendida sin cambiar su código.
El Principio Abierto/Cerrado permite construir bases de código evolutivas. En los próximos capítulos exploraremos cómo se complementa con el Principio de Sustitución de Liskov para mantener jerarquías consistentes.