Principios SOLID. Open Closed principle


Principios SOLID. Open Closed principle

Revisamos el concepto Open/Closed, el segundo de los principios SOLID. Una clase debe estar abierta a la extensión pero cerrada a la modificación.



Algo de historia


Este principio fue nombrado por primera vez por Bertrand Mayer en su libro Object-Oriented Software Construction

Principio Abierto / Cerrado


Los objetos o entidades deberían estar abiertas a su extensión, pero cerradas para su modificación.:

Principio Abierto / Cerrado

Es decir, una clase debe estar abierta a la extensión pero cerrada a la modificación. En una clase deberíamos poder seguir agregando funcionalidades sin cambiar el código ya escrito y menos alterando su responsabilidad o razón de ser.
Si se aplica correctamente el principio de responsabilidad única a nuestras clases, podríamos agregar comportamientos o funcionalidades a las mismas sin afectar el primer principio. Esto sin embargo no es una ley y que cumpliendo el primer principio se cumpla automáticamente el segundo no siempre sera valido.

Conociendo el problema


Siguiendo con el ejemplo del articulo anterior revisemos el método calcularAreas() de la clase AreaCalculadorPRU
private function calcularAreas() {
foreach ($this->figuras as $figura) {
if(is_a($figura, 'Blockpc\Clases\Cuadrado')){
$this->area[] = pow($figura->getLado(), 2);
} elseif (is_a($figura, 'Blockpc\Clases\Circulo')){
$this->area[] = pi() * pow($figura->getRadio(), 2);
} else {
throw new \Exception("Se esperaba un objeto figura!");
}
}
}

¿Que pasa si necesitamos agregar mas figuras, un triangulo, un rectángulo, ...? Tendríamos que seguir agregando bloques if-else según tantas figuras necesitemos, aplicando su solución matemática para el calculo de área, lo que va en contra del segundo principio (Cerrado para modificaciones) pues estaríamos añadiendo código (no funcionalidad) para cumplir con la funcionalidad de la clase.

Solución


Pues existen dos, ambas aplican este segundo principio de agregar funcionalidad y se aplican a las clases Circulo y Cuadrado.
La primera es la creación de una clase abstracta AbstractAreas que obligue a estas clases implementar un método para calcular sus propias áreas.
/* Clase AbstractAreas */
namespace Blockpc\Clases;
abstract class AbstractAreas {
public abstract function calcularArea();
}

Asi por ejemplo nuestra clase mejorada CirculoAbstracto, antes llamada Circulo, quedaría:
/* Clase CirculoAbstracto */
namespace Blockpc\Clases;
class CirculoAbstracto extends AbstractAreas {
private $radio;
public function __construct(int $radio) {
$this->radio = $radio;
}
public function getRadio() {
return $this->radio;
}
public function calcularArea() {
return pi() * pow($this->radio, 2);
}
}

La segunda es la creación de una Interface InterfaceAreas que defina una función para calcular las áreas de las figuras.
/* Clase InterfaceAreas */
namespace Blockpc\Clases;
Interface InterfaceAreas {
public abstract function calcularArea();
}

En este caso nuestra clase mejorada CirculoInteface, antes llamada Circulo, quedaría:
/* Clase CirculoInterface */
namespace Blockpc\Clases;
class CirculoInterface implements InterfaceAreas {
private $radio;
public function __construct(int $radio) {
$this->radio = $radio;
}
public function getRadio() {
return $this->radio;
}
public function calcularArea() {
return pi() * pow($this->radio, 2);
}
}

Y para que todo tenga sentido, nuestra clase mejorada AreaCalculadorPOC
/* Clase AreaCalculadorPOC */
namespace Blockpc\Clases;
class AreaCalculadorPOC {
private $figuras;
public function __construct(...$figuras) {
$this->figuras = $figuras;
}
private function calcularAreas() {
foreach ($this->figuras as $figura) {
/* Cada objeto figura llama a su propio método calcularArea() */
$this->area[] = $figura->calcularArea();
}
}
public function getAreas() {
$this->calcularAreas();
return $this->area;
}
}

Los mismos cambios se aplican a la clase Cuadrado, así veras en el código fuente dos nuevas clases CuadradoAbstracto y CuadradoInterface

Aplicando la funcionalidad


En el archivo index.php verán algo así:
/*
* Cuarto ejemplo funcional
* Aplicando el Principio de Abierto / Cerrado
* Solución con una clase abstracta
**/
$circuloAbs = new Blockpc\Clases\CirculoAbstracto(4);
$cuadradoAbs = new Blockpc\Clases\CuadradoAbstracto(4);
$area = new Blockpc\Clases\AreaCalculadorPOC($circuloAbs, $cuadradoAbs);
$areas = $area->getAreas();
$suma = new Blockpc\Clases\SumaCalculadorPRU($areas);
echo $suma->getSuma();
/*
* Quinto ejemplo funcional
* Aplicando el Principio de Abierto / Cerrado
* Solución con una Interface
**/
$circuloInt = new Blockpc\Clases\CirculoInterface(4);
$cuadradoInt = new Blockpc\Clases\CuadradoInterface(4);
$area = new Blockpc\Clases\AreaCalculadorPOC($circuloInt, $cuadradoInt);
$areas = $area->getAreas();
$suma = new Blockpc\Clases\SumaCalculadorPRU($areas);
echo $suma->getSuma();

El código fuente ira como siempre adjunto al final del articulo.

Ventajas y desventajas de las soluciones


Cuando la solución que elegimos se basa en una clase abstracta, debemos entender que las clases que crearemos por cada figura deberán extender de esta y sabemos que cada clase solo puede extender de una, esto nos limita ante posibles mejoras de nuestro desarrollo y si por ejemplo, necesitáramos calcular los volúmenes de esas figuras, tendríamos que agregar otro método abstracto a la clase para cumplir con esa nueva funcionalidad lo que de paso violaría el primer principio (una única responsabilidad) para la clase abstracta.
La solución que implementa una interface no nos limita a una sola interface pues sabemos que una clase puede implementar todas las interfaces que requiera. Así, podríamos crear otra interface que defina un método para calcular los volúmenes de las figuras y luego implementarlos en cada clase. Por otro lado, aseguramos la posibilidad de extender de una clase padre si en algún futuro se necesita.
Recuerden que esto no es ley y que cualquier "mejor" solución elegida solo va a depender del proyecto y la practica de aplicar estos principios.

Agregando una figura


Si ahora necesitamos calcular ademas el área de un triangulo, agregamos la clase Triangulo tendríamos
/* Clase TrianguloAbstracto */
namespace Blockpc\Clases;
class TrianguloAbstracto extends AbstractAreas {
private $base;
private $altura;
public function __construct(int $base, int $altura) {
$this->base = $base;
$this->altura = $altura;
}
public function calcularArea() {
return ($this->base * $this->altura)/2;
}
}

EL código para la clase TrianguloInterface sera:
/* Clase TrianguloInterface  */
namespace Blockpc\Clases;
class TrianguloInterface implements InterfaceAreas {
private $base;
private $altura;
public function __construct(int $base, int $altura) {
$this->base = $base;
$this->altura = $altura;
}
public function calcularArea() {
return ($this->base * $this->altura)/2;
}

Como pueden ver, en ambos casos bastaría con implementar correctamente el método calcularArea() que nos obligan tanto la clase AbstractAreas como la interface InterfaceAreas

Un detalle a tener en cuenta


Ahora bien, debemos asegurarnos que sean figuras a las que calcularemos el área y ademas tengan implementado el método calcularArea(), sino estaríamos ante serios problemas jejeje.
Lo solucionamos agregando una pequeña validación en la nueva clase AreaCalculadorFigurasInterfacesPOC para aquellas figuras que implementen la interface.
/* Clase AreaCalculadorFigurasInterfacesPOC */
namespace Blockpc\Clases;
class AreaCalculadorFigurasInterfacesPOC {
private $figuras;
public function __construct(...$figuras) {
$this->figuras = $figuras;
}
private function calcularAreas() {
foreach ($this->figuras as $figura) {
/* Validación del objeto */
if ( $figura instanceof InterfaceAreas ) {
$this->area[] = $figura->calcularArea();
continue;
}
throw new \Exception("Se esperaba una figura implementando la Interface");
}
}
public function getAreas() {
$this->calcularAreas();
return $this->area;
}
}

Y en la nueva clase AreaCalculadorFigurasAbstractasPOC que usaremos para validar aquellas figuras que extiendan de la clase abstracta.
/* Clase AreaCalculadorFigurasAbstractasPOC */
namespace Blockpc\Clases;
class AreaCalculadorFigurasAbstractasPOC {
private $figuras;
public function __construct(...$figuras) {
$this->figuras = $figuras;
}
private function calcularAreas() {
foreach ($this->figuras as $figura) {
/* Validación del objeto */
if ( is_a($figura, 'Blockpc\Clases\AbstractAreas') ) {
$this->area[] = $figura->calcularArea();
continue;
}
throw new \Exception("Se esperaba una figura");
}
}
public function getAreas() {
$this->calcularAreas();
return $this->area;
}
}


Ultimas consideraciones


Recordar que el uso de una clase abstracta o de una interface solo dependerá del código que vayan creando y de las necesidades del desarrollo, pero solo puede existir una de las dos soluciones.
Para el caso de este articulo, vemos las dos y en el código fuente irán todas las clases que hemos visto.

Puedes revisar la documentación del método is_a de PHP desde acá.
La documentación del método instanceof desde acá

En el código fuente lo descargas desde acá

Etiquetas solid

Ultima actualización Viernes 04 de Mayo, 2018




Agregar Comentario