Principio abierto/cerrado
Introducción
El Principio Abierto-Cerrado (OCP, por sus siglas en inglés) es uno de los principios SOLID de diseño de software orientado a objetos. Fue acuñado en 1988 por Bertrand Meyer y establece que:
Un artefacto de software debe estar abierto para su extensión, pero cerrado para su modificación.
En otras palabras, el comportamiento de un artefacto de software debe poder extenderse sin tener que modificar el artefacto en sí. Este principio es fundamental en la arquitectura de software y tiene implicaciones significativas tanto a nivel de clases y módulos como a nivel de componentes arquitectónicos.
Importancia del OCP
El OCP es crucial por las siguientes razones:
- Flexibilidad: Permite agregar nuevas funcionalidades sin alterar el código existente.
- Mantenibilidad: Reduce el riesgo de introducir errores en código que ya funciona.
- Escalabilidad: Facilita el crecimiento y evolución del sistema a lo largo del tiempo.
Si un sistema no sigue el OCP, pequeños cambios en los requisitos podrían forzar modificaciones masivas en el software, lo que indicaría un fallo significativo en su arquitectura.
Un experimento mental
Para entender mejor la aplicación del OCP a nivel arquitectónico, consideremos el siguiente escenario:
Imaginemos que tenemos un sistema en Java que muestra un resumen contable en una página web. Los datos son desplazables y los números negativos se muestran en rojo. Ahora, los interesados solicitan que esta misma información se convierta en un informe para imprimir en una impresora en blanco y negro. El informe debe estar correctamente paginado, con encabezados y pies de página apropiados, y etiquetas de columna. Los números negativos deben estar entre paréntesis.
La pregunta clave es: ¿cuánto código existente tendrá que cambiar para implementar esta nueva funcionalidad?
Una buena arquitectura de software reduciría la cantidad de código modificado al mínimo absoluto, idealmente a cero. ¿Cómo se logra esto? Aplicando dos principios fundamentales:
- El Principio de Responsabilidad Única (SRP): Separando las cosas que cambian por diferentes razones (visto anteriormente).
- El Principio Abierto-Cerrado (OCP): Organizando el código de manera que sea extensible sin necesidad de modificación.
Aplicación práctica en Java
Veamos cómo podemos aplicar el OCP para diseñar una arquitectura flexible y extensible en Java.
Paso 1: Separación de responsabilidades (SRP)
Aplicando el Principio de Responsabilidad Única (SRP), podemos identificar dos responsabilidades principales en nuestro sistema:
- El cálculo de los datos financieros.
- La presentación de esos datos en diferentes formatos (web e impreso).
Esta separación nos permite crear un flujo de datos como el siguiente:
[Datos Financieros] -> [Análisis] -> [Datos Reportables] -> [Formato Web] -> [Formato Impreso]
Paso 2: Implementación de la arquitectura (OCP)
Veamos cómo podría ser la implementación de esta arquitectura en Java:
1// Interfaces2interface FinancialDataGateway {3 List<FinancialData> getFinancialData();4}5
6interface FinancialReportPresenter {7 String present(List<FinancialData> data);8}9
10// Clase de datos11class FinancialData {12 // Atributos y métodos...13}14
15// Interactor16class FinancialReportGenerator {17 private final FinancialDataGateway dataGateway;18
19 public FinancialReportGenerator(FinancialDataGateway dataGateway) {20 this.dataGateway = dataGateway;21 }22
23 public List<FinancialData> generateReport() {24 List<FinancialData> data = dataGateway.getFinancialData();25 // Procesar datos...26 return processedData;27 }28}29
30// Presentadores31class WebPresenter implements FinancialReportPresenter {32 @Override33 public String present(List<FinancialData> data) {34 // Formatear datos para web35 return webFormattedData;36 }37}38
39class PrintPresenter implements FinancialReportPresenter {40 @Override41 public String present(List<FinancialData> data) {42 // Formatear datos para impresión43 return printFormattedData;44 }45}46
47// Controlador48class FinancialReportController {49 private final FinancialReportGenerator generator;50 private final FinancialReportPresenter presenter;51
52 public FinancialReportController(FinancialReportGenerator generator, FinancialReportPresenter presenter) {53 this.generator = generator;54 this.presenter = presenter;55 }56
57 public String createReport() {58 List<FinancialData> data = generator.generateReport();59 return presenter.present(data);60 }61}62
63// Uso64public class Main {65 public static void main(String[] args) {66 FinancialDataGateway dataGateway = new ConcreteFinancialDataGateway();67 FinancialReportGenerator generator = new FinancialReportGenerator(dataGateway);68
69 FinancialReportPresenter webPresenter = new WebPresenter();70 FinancialReportController webController = new FinancialReportController(generator, webPresenter);71 String webReport = webController.createReport();72
73 FinancialReportPresenter printPresenter = new PrintPresenter();74 FinancialReportController printController = new FinancialReportController(generator, printPresenter);75 String printReport = printController.createReport();76 }77}
Análisis de la implementación
-
Extensibilidad: Para añadir un nuevo formato de reporte (por ejemplo, PDF), solo necesitamos crear una nueva clase
PDFPresenter
que implementeFinancialReportPresenter
. No se requiere modificar el código existente. -
Separación de responsabilidades: Cada clase tiene una única responsabilidad. Por ejemplo,
FinancialReportGenerator
se encarga solo de generar los datos del reporte, mientras que los presentadores se encargan únicamente de formatear esos datos. -
Protección de componentes: El
FinancialReportGenerator
está protegido de los cambios en los componentes de presentación. Puede generar reportes sin saber cómo se presentarán.
Esta implementación permite que nuestro sistema sea abierto para la extensión (podemos añadir nuevos formatos de presentación) pero cerrado para la modificación (no necesitamos cambiar el código existente para añadir estas nuevas funcionalidades).
Ejemplo de aplicación: Sistema de Gestión de Empleados
Imaginemos que estamos desarrollando un sistema de gestión de empleados para una empresa. Inicialmente, el sistema calcula los salarios de los empleados regulares. Sin embargo, la empresa está creciendo y ahora necesita manejar diferentes tipos de empleados, cada uno con su propio cálculo de salario.
Versión inicial (sin OCP)
Primero, veamos cómo podría verse una implementación que no sigue el OCP:
1public class Empleado {2 private String nombre;3 private String tipo; // "regular", "contratista", "gerente", "supervisor", etc.4 private double salarioBase;5
6 public Empleado(String nombre, String tipo, double salarioBase) {7 this.nombre = nombre;8 this.tipo = tipo;9 this.salarioBase = salarioBase;10 }11
12 public double calcularSalario() {13 if (tipo.equals("regular")) {14 return salarioBase;15 } else if (tipo.equals("contratista")) {16 return salarioBase * 1.5;17 } else if (tipo.equals("gerente")) {18 return salarioBase * 2 + 1000;19 } else if (tipo.equals("supervisor")) {20 return salarioBase * 1.8;21 } else {22
23 throw new IllegalArgumentException("Tipo de empleado desconocido");24 }25
26 // Getters y setters...27}28
29public class CalculadoraNomina {30 public double calcularNominaTotalMensual(List<Empleado> empleados) {31 double total = 0;32 for (Empleado empleado : empleados) {33 total += empleado.calcularSalario();34 }35 return total;36 }37}
Esta implementación viola el OCP
porque cada vez que se añade un nuevo tipo de empleado, necesitamos modificar la clase Empleado
y su método calcularSalario()
.
Aplicando el OCP
Ahora, veamos cómo podemos rediseñar este sistema aplicando el OCP:
1// Interfaz para el cálculo de salario2public interface CalculadoraSalario {3 double calcularSalario(double salarioBase);4}5
6// Implementaciones concretas para cada tipo de empleado7public class CalculadoraSalarioRegular implements CalculadoraSalario {8 @Override9 public double calcularSalario(double salarioBase) {10 return salarioBase;11 }12}13
14public class CalculadoraSalarioContratista implements CalculadoraSalario {15 @Override16 public double calcularSalario(double salarioBase) {17 return salarioBase * 1.5;18 }19}20
21public class CalculadoraSalarioGerente implements CalculadoraSalario {22 @Override23 public double calcularSalario(double salarioBase) {24 return salarioBase * 2 + 1000;25 }26}27
28// Clase Empleado refactorizada29public class Empleado {30 private String nombre;31 private double salarioBase;32 private CalculadoraSalario calculadoraSalario;33
34 public Empleado(String nombre, double salarioBase, CalculadoraSalario calculadoraSalario) {35 this.nombre = nombre;36 this.salarioBase = salarioBase;37 this.calculadoraSalario = calculadoraSalario;38 }39
40 public double calcularSalario() {41 return calculadoraSalario.calcularSalario(salarioBase);42 }43
44 // Getters y setters...45}46
47// La clase CalculadoraNomina no necesita modificaciones48public class CalculadoraNomina {49 public double calcularNominaTotalMensual(List<Empleado> empleados) {50 double total = 0;51 for (Empleado empleado : empleados) {52 total += empleado.calcularSalario();53 }54 return total;55 }56}
Mejoras realizadas
-
Extensibilidad: Ahora podemos añadir nuevos tipos de empleados sin modificar las clases existentes. Por ejemplo, si queremos añadir un tipo de empleado “Ejecutivo”:
1public class CalculadoraSalarioEjecutivo implements CalculadoraSalario {2@Override3public double calcularSalario(double salarioBase) {4return salarioBase * 3 + 2000;5}6}No necesitamos modificar
Empleado
,CalculadoraNomina
, ni ninguna otra clase existente. -
Flexibilidad: Podemos cambiar fácilmente el cálculo del salario para un empleado en tiempo de ejecución:
1Empleado juan = new Empleado("Juan", 3000, new CalculadoraSalarioRegular());2// Más adelante...3juan.setCalculadoraSalario(new CalculadoraSalarioGerente()); -
Mantenibilidad: Cada tipo de cálculo de salario está encapsulado en su propia clase, lo que hace que el código sea más fácil de mantener y probar.
-
Principio de Responsabilidad Única (SRP): Cada clase tiene una única razón para cambiar. Por ejemplo,
CalculadoraSalarioContratista
solo cambiará si la fórmula para calcular el salario de los contratistas cambia.
Uso del sistema
1public class SistemaGestionEmpleados {2 public static void main(String[] args) {3 List<Empleado> empleados = new ArrayList<>();4 empleados.add(new Empleado("María", 3000, new CalculadoraSalarioRegular()));5 empleados.add(new Empleado("Carlos", 4000, new CalculadoraSalarioContratista()));6 empleados.add(new Empleado("Ana", 5000, new CalculadoraSalarioGerente()));7
8 CalculadoraNomina calculadora = new CalculadoraNomina();9 double nominaTotal = calculadora.calcularNominaTotalMensual(empleados);10
11 System.out.println("La nómina total mensual es: " + nominaTotal);12 }13}
Conclusiones del ejemplo
Este ejemplo demuestra cómo la aplicación del OCP nos permite crear un sistema que es fácilmente extensible. Podemos añadir nuevos tipos de empleados y cálculos de salario sin modificar el código existente, simplemente creando nuevas clases que implementen la interfaz CalculadoraSalario
.
Además, esta estructura facilita la aplicación de otros principios SOLID, como el Principio de Responsabilidad Única (SRP), lo que resulta en un diseño más robusto y mantenible.
Conclusión
El Principio Abierto-Cerrado nos permite diseñar sistemas flexibles y fáciles de mantener. Al separar las responsabilidades y utilizar interfaces, podemos crear una arquitectura que sea resistente a los cambios y fácil de extender. En nuestro primer ejemplo, podemos agregar nuevos formatos de presentación sin modificar el código existente, cumpliendo así con el OCP.
La aplicación del OCP en Java nos permite crear sistemas más modulares y menos propensos a errores, facilitando el mantenimiento y la evolución del software a lo largo del tiempo.