Open/Close principle
Hoy vamos a hablar sobre el segundo principio de los ya conocidos principios SOLID, este es el “Open/Close Principle” o “Principio abierto/cerrado”
Este principio fue creado por Bertrand Mayer, quien lo introdujo en su libro “Object Oriented Software Construction” en el año 1988.
¿Qué quiere decir esto? Quiere decir que nuestras clases, tienen que ser capaces de estar abiertas a extender el comportamiento sin necesidad de modificar su código.
Como no realizamos modificaciones en las clases existentes, podemos tener seguridad que las nuevas extensiones no van a afectar a los desarrollos ya completados.
¿Cómo detectar que estamos violando el principio Abierto/Cerrado?
Una de las formas más sencillas de detectar que estamos violando el principio Abierto/Cerrado es que cuando añades funcionalidad, acabas modificando siempre los mismos archivos. Si detectamos este patrón, tendremos que hacer una pausa, entender por qué nos ocurre y realizar una refactorización para cumplir con el principio.
¿Cómo solucionar la violación del principio?
La forma más fácil de solucionar el principio Abierto/Cerrado, es mediante polimorfismo. Con polimorfismo, en lugar de tener una clase principal que es capaz de saber cómo realizar una operación, delega la lógica a los objetos que conocen como solucionar esta lógica. Cada objeto, implementará una forma específica de resolución de la operación y según el tipo de operación se llamará al objeto encargado para solucionarlo.
Ejemplo de solución de principio Abierto/Cerrado
Tenemos una clase rectángulo, con un método que calcula el área de esta figura geométrica.
class Rectangle {
constructor(private _width: number, private readonly _height: number) {}
get width(): number {
return this._width;
}
get height(): number {
return this._height;
}
}
Por otro lado, tenemos una clase, que dado un array de rectángulos, calcula todas sus áreas y devuelve el resultado sumado
class AreaCalculator {
constructor(private rectangles: Rectangle[]) {}
calculate() {
let area = 0;
for (const rectangle of this.rectangles) {
area = area + rectangle.height * rectangle.width
}
return area;
}
}
Esta clase no está abierta a extensión y cerrada a modificación ya que si ahora añadimos la clase círculo tendríamos que modificar la clase.
class Circle {
constructor(private _radius: number) {}
get radius(): number {
return this._radius;
}
}
Si nos fijamos, hemos tenido que añadir un if/else dentro del método de calcular el área. Si quisiéramos ahora añadir un triángulo, tendríamos la misma problemática.
class AreaCalculator {
constructor(private shapes: Rectangle[] | Circle[]) {}
calculate() {
let area = 0;
for (const shape of this.shapes) {
if (shape instanceof Rectangle) {
area = area + shape.height * shape.width;
} else {
area = area + shape.radius * shape.radius * Math.PI;
}
}
return area;
}
}
¿Cómo podemos solucionar esto?
Creamos una interfaz que todas las clases que necesiten calcular el área la implementen. Con esto, todas tendrán el método calculateArea().
interface Shape {
calculaeArea(): number;
}
Las clases Rectangle y Circle ahora quedarían:
class Rectangle implements Shape {
constructor(private _width: number, private readonly _height: number) {}
get width(): number {
return this._width;
}
get height(): number {
return this._height;
}
calculateArea(): number {
return this._height * this._width;
}
}
class Circle implements Shape {
constructor(private _radius: number) {}
get radius(): number {
return this._radius;
}
calculateArea(): number {
return this._radius * this._radius * Math.PI;
}
}
Y la clase de calcular las áreas:
class AreaCalculator {
constructor(private shapes: Shape[]) {}
calculate() {
let area = 0;
for (const shape of this.shapes) {
area = area + shape.calculaeArea();
}
return area;
}
}
Ahora, el tipo de datos que se usan en la clase don del tipo de la interfaz, por lo que todas la clases que la implementan tienen el método calculateArea().
SI ahora añadiéramos por ejemplo, la clase Triangle, tendríamos que hacer que la clase Triangle implementara la interfaz Shape y no necesitaríamos hacer ninguna modificación en la clase AreaCalculator.
Este principio, al igual que el principio de Single Responsibility que exploramos anteriormente en nuestro blog, subraya la importancia de un diseño modular y adaptable. Al adherirnos a estos principios, facilitamos la escalabilidad y mantenibilidad del software, preparándolo para futuras expansiones o cambios.
En siguientes posts, profundizaremos en otros principios SOLID, hablaremos el Liskov Substitution, para continuar construyendo una base sólida en el diseño de software orientado a objetos. Mantente atento para más insights y mejores prácticas en el ámbito.