En este post vamos a ver un ejemplo de diseño que no cumple con el principio Open-Closed y una versión del mismo ejemplo que sí cumple con el principio.
Principio Open-Closed (OCP)
Los principios SOLID son cinco principios básicos del diseño y la programación orientada a objetos, uno de los cuales es el principio Open-Closed
Una descripción más detallada de este principio la pueden encontrar en un artículo de Robert C. Martin “Uncle Bob”, coautor del Manifiesto Ágil, titulado The Open-Closed Principle.
Bertrand Meyer acuñó este principio en 1988 de la siguiente manera:
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
Y en su artículo, Uncle Bob nos explica que:
Los módulos que cumplen con el principio open-closed tienen dos atributos principales:
- Están “abiertos para extensiones” (Open).
- Esto significa que el comportamiento de un módulo puede ser extendido. Que podemos hacer que un módulo se comporte de nuevas y diferentes maneras a medida en que los requerimientos de la aplicación cambien, o para satisfacer nuevas aplicaciones.
- Están “cerrados para modificaciones” (Closed).
- El código fuente de dicho módulo es inviolable. Nadie está autorizado a cambiar dicho código.
Ejemplo que no cumple con OCP
Vamos primero a ver un ejemplo que no cumple con OCP. El ejemplo es una “aplicación” que permite calcular el área que ocupan un grupo de figuras en un plano (el siguiente código puede ser ejecutado en Xcode Playground):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
class Figura { | |
} | |
class Rectangulo: Figura { | |
var alto: Double | |
var largo: Double | |
init(alto: Double, largo: Double) { | |
self.alto = alto | |
self.largo = largo | |
} | |
} | |
class Circulo: Figura { | |
var radio: Double | |
init(radio: Double) { | |
self.radio = radio | |
} | |
} | |
class CalculadorDeAreas { | |
var figuras: [Figura] | |
init(figuras: [Figura]) { | |
self.figuras = figuras | |
} | |
private func calcularAreaRectangulo(rectangulo : Rectangulo) -> Double { | |
return rectangulo.alto * rectangulo.largo | |
} | |
private func calcularAreaCirculo(circulo : Circulo) -> Double { | |
let pi = 3.14159 | |
return pi * pow(circulo.radio, 2) | |
} | |
func calcular() -> Double { | |
var area = 0.0 | |
for figura in self.figuras { | |
if (figura is Rectangulo) { | |
area += calcularAreaRectangulo(figura as! Rectangulo) | |
} else { | |
area += calcularAreaCirculo(figura as! Circulo) | |
} | |
} | |
return area | |
} | |
} | |
let figuras = [Rectangulo(alto: 3, largo: 4), Circulo(radio: 5)] | |
let calculadorDeAreas = CalculadorDeAreas(figuras: figuras) | |
print("El área es \(calculadorDeAreas.calcular())") |
Como podemos ver en el código anterior, la función calcular y la clase CalculadorDeAreas no cumple con OCP debido a que si queremos tener otros tipos de figuras en nuestra aplicación, y que podamos utilizarlas en el cálculo del área debemos modificar el código de la clase CalculadorDeAreas para introducir métodos para calcular las áreas de estos nuevos tipos de figuras, pero aún resolviendo este problema, tendríamos que modificar el método calcular para agregar nuevos if-else que tengan en cuenta los nuevos tipos de figuras.
¿Cómo podemos refactorizar el código anterior para hacerlo cumplir con OCP?
Ejemplo que cumple con OCP
El siguiente código es una refactorización del ejemplo anterior:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
protocol TieneArea { | |
func calcularArea() -> Double | |
} | |
class Rectangulo: TieneArea { | |
var alto: Double | |
var largo: Double | |
init(alto: Double, largo: Double) { | |
self.alto = alto | |
self.largo = largo | |
} | |
func calcularArea() -> Double { | |
return alto * largo | |
} | |
} | |
class Circulo: TieneArea { | |
var radio: Double | |
let pi = 3.14159 | |
init(radio: Double) { | |
self.radio = radio | |
} | |
func calcularArea() -> Double { | |
return pi * pow(radio, 2) | |
} | |
} | |
class CalculadorDeAreas { | |
var figuras: [TieneArea] | |
init(figuras: [TieneArea]) { | |
self.figuras = figuras | |
} | |
func calcular() -> Double { | |
var area = 0.0 | |
for figura in self.figuras { | |
area += figura.calcularArea() | |
} | |
return area | |
} | |
} | |
let calculadorDeAreas = CalculadorDeAreas(figuras: [Rectangulo(alto: 3, largo: 4), Circulo(radio: 5)]) | |
print("El área es \(calculadorDeAreas.calcular())") |
En esta versión utilizamos un protocolo: TieneArea, que define a la función calcularArea, y las clases Rectangulo y Circulo implementan este protocolo brindando una implementación propia para el cálculo del área según el tipo de figura que las mismas representan. De este modo si queremos extender el funcionamiento de la clase CalculadorDeAreas y su función calcular, no necesitamos modificar las mismas sino que basta con crear una nueva clase que representa la nueva figura y hacerla implementar el protocolo TieneArea.
[…] de diseño ya que rompe con el principio “Open/Closed” de SOLID, hace un tiempo escribí sobre este principio en Swift pero la idea es la misma de forma general. Entonces vamos a extraer esto hacia un proveedor y para […]