Saltearse al contenido

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:

  1. Flexibilidad: Permite agregar nuevas funcionalidades sin alterar el código existente.
  2. Mantenibilidad: Reduce el riesgo de introducir errores en código que ya funciona.
  3. 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:

  1. El Principio de Responsabilidad Única (SRP): Separando las cosas que cambian por diferentes razones (visto anteriormente).
  2. 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:

  1. El cálculo de los datos financieros.
  2. 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
// Interfaces
2
interface FinancialDataGateway {
3
List<FinancialData> getFinancialData();
4
}
5
6
interface FinancialReportPresenter {
7
String present(List<FinancialData> data);
8
}
9
10
// Clase de datos
11
class FinancialData {
12
// Atributos y métodos...
13
}
14
15
// Interactor
16
class 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
// Presentadores
31
class WebPresenter implements FinancialReportPresenter {
32
@Override
33
public String present(List<FinancialData> data) {
34
// Formatear datos para web
35
return webFormattedData;
36
}
37
}
38
39
class PrintPresenter implements FinancialReportPresenter {
40
@Override
41
public String present(List<FinancialData> data) {
42
// Formatear datos para impresión
43
return printFormattedData;
44
}
45
}
46
47
// Controlador
48
class 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
// Uso
64
public 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

  1. Extensibilidad: Para añadir un nuevo formato de reporte (por ejemplo, PDF), solo necesitamos crear una nueva clase PDFPresenter que implemente FinancialReportPresenter. No se requiere modificar el código existente.

  2. 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.

  3. 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:

1
public 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
29
public 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 salario
2
public interface CalculadoraSalario {
3
double calcularSalario(double salarioBase);
4
}
5
6
// Implementaciones concretas para cada tipo de empleado
7
public class CalculadoraSalarioRegular implements CalculadoraSalario {
8
@Override
9
public double calcularSalario(double salarioBase) {
10
return salarioBase;
11
}
12
}
13
14
public class CalculadoraSalarioContratista implements CalculadoraSalario {
15
@Override
16
public double calcularSalario(double salarioBase) {
17
return salarioBase * 1.5;
18
}
19
}
20
21
public class CalculadoraSalarioGerente implements CalculadoraSalario {
22
@Override
23
public double calcularSalario(double salarioBase) {
24
return salarioBase * 2 + 1000;
25
}
26
}
27
28
// Clase Empleado refactorizada
29
public 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 modificaciones
48
public 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

  1. 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”:

    1
    public class CalculadoraSalarioEjecutivo implements CalculadoraSalario {
    2
    @Override
    3
    public double calcularSalario(double salarioBase) {
    4
    return salarioBase * 3 + 2000;
    5
    }
    6
    }

    No necesitamos modificar Empleado, CalculadoraNomina, ni ninguna otra clase existente.

  2. Flexibilidad: Podemos cambiar fácilmente el cálculo del salario para un empleado en tiempo de ejecución:

    1
    Empleado juan = new Empleado("Juan", 3000, new CalculadoraSalarioRegular());
    2
    // Más adelante...
    3
    juan.setCalculadoraSalario(new CalculadoraSalarioGerente());
  3. 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.

  4. 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

1
public 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.