Principios SOLID. Liskov substitution principle


Principios SOLID. Liskov substitution principle

Revisamos el tercer concepto de los principios SOLID. El principio de sustitución de Liskov. Cualquier objeto de una clase hija debería poder ser sustituible por la clase padre.



Algo de historia


La primera persona en hablar de este principio, fue una mujer llamada Barbara Liskov y este principio lleva orgulloso su apellido.

Principio de Sustitución de Liskov


Si en alguna parte de nuestro código estamos usando una clase, y esta clase es extendida, tenemos que poder utilizar cualquiera de las clases hijas y que el programa siga siendo válido:

Principio de Sustitución de Liskov

Un meme sobre el Principio de Sustitución de Liskov
Un meme sobre el Principio de Sustitución de Liskov

Esto se lee algo confuso y es así, el principio habla de la jerarquía de clases, la herencia y el buen uso de la abstracción de las mismas.
Si una clase hija extiende de la clase madre entonces todos las propiedades y métodos heredados de la clase madre deben tener un propósito en la clase hija.
Si una clase hija hereda una funcionalidad que no ocupa estaría violando este principio. Es mas, una funcionalidad heredada sin utilidad es claramente una violación al primer principio SOLID.

Situación actual


En el articulo anterior hablamos de que pasaría si necesitáramos calcular los volúmenes de los objetos figura y vimos las posibilidades de ampliar la funcionalidad de la clase padre o de implementar una nueva interface.
Para poder obtener el volumen de una figura, necesitaremos una propiedad mas, la altura de cada una de estas y le pondremos un valor por defecto de cero. Para las clases Cuadrado, Triangulo y la nueva clase Rectángulo tendríamos que agregar el mismo atributo.
¿Pero no estaríamos violando el segundo principio agregando esta propiedad? Si, y es por eso la importancia de captar correctamente los requerimientos del proyecto pero al ser este un ejemplo, tomaremos como si empezáramos de cero jejeje.
Así nuestra clase Circulo quedaría:
/* Clase Circulo */
namespace Blockpc\Clases;
class Circulo extends Figuras {
private $radio;
private $altura; /* Nueva propiedad */
public function __construct(int $radio, int $altura = 0) {
$this->radio = $radio;
$this->altura = $altura; /* Nueva propiedad */
}
public function getRadio() {
return $this->radio;
}
public function getAltura() { /* Nuevo método */
return $this->altura;
}
public function calcularArea() {
return pi() * pow($this->radio, 2);
}
}

La clase Figuras es nuestra conocida AbstractAreas, solo le cambie el nombre a uno mas relacionado con el problema, esta clase seria:
/* Clase Figuras */
namespace Blockpc\Clases;
abstract class Figuras {
public abstract function calcularArea();
}

Recordemos la clase para calcular las áreas CalculadorArea, que recibe como parámetro uno o más objetos hijos de Figuras, sera:
/* Clase CalculadorArea */
namespace Blockpc\Clases;
class CalculadorArea {
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\Figuras') ) {
$areas[] = $figura->calcularArea();
continue;
}
throw new \Exception("Se esperaba una figura");
}
return $areas;
}
}

Y por ultimo la clase que suma las áreas, esta clase recibe un arreglo con los valores de áreas ya calculados
/* Clase CalculadorSuma */
namespace Blockpc\Clases;
class CalculadorSuma {
private $areas;
public function __construct(array $areas) {
$this->areas = $areas;
}
public function getSuma() {
return array_sum($this->areas);
}
}


El sistema actual


Creamos una clase CalculadorVolumenes que realice los cálculos para el volumen de las figuras. Recibe como parametros, los objetos Figuras a los que calcular el volumen.
/* Clase CalculadorVolumenes */
namespace Blockpc\Clases;
class CalculadorVolumenes {
private $figuras;
public function __construct(...$figuras) {
$this->figuras = $figuras;
}
public function calcularVolumenes() {
foreach ($this->figuras as $figura) {
/* Validación del objeto */
if ( is_a($figura, 'Blockpc\Clases\Figuras') ) {
$volumenes[] = $figura->calcularArea() * $figura->getAltura() ?? 0;
continue;
}
throw new \Exception("Se esperaba una figura");
}
return $volumenes;
}
}

En nuestro archivo index.php entonces, definimos los objetos, calculamos las áreas y luego los volúmenes:

/* Definimos los objetos */
$circulo = new Blockpc\Clases\Circulo(4, 2);
$cuadrado = new Blockpc\Clases\Cuadrado(4, 2);
$triangulo = new Blockpc\Clases\Triangulo(4, 3, 2);
$rectangulo = new Blockpc\Clases\Rectangulo(4, 3, 2);
/* Calculamos la suma de las áreas creando un objeto CalculadorArea
* y sumamos en CalculadorSuma */
$area = new Blockpc\Clases\CalculadorArea($circulo, $cuadrado, $triangulo, $rectangulo);
$areas = $area->getAreas();
$sumaAreas = new Blockpc\Clases\CalculadorSuma($areas);
/* Calculamos la suma de los volúmenes creando un objeto CalculadorVolumenes
* y sumamos en CalculadorSuma */
$volumen = new Blockpc\Clases\CalculadorVolumenes($circulo, $cuadrado, $triangulo, $rectangulo);
$volumenes = $volumen->getVolumen();
$sumaVolumenes = new Blockpc\Clases\CalculadorSuma($volumenes);


El problema real


la solución anterior es buena y funciona, pero aun no aplica el Principio de Sustitución de Liskov.
¿Podriamos aplicar este principio a la clase Figuras y sus hijas?
No, pues Figuras la hemos declarado abstracta y con esto hemos resuelto que serán las clases hijas quienes implementen su método para calcular las áreas.
¿Entonces donde se aplica este principio?
Debemos entender que la clase CalculadorSuma esta recibiendo un arreglo de valores proporcionado tanto por la clase CalculadorArea como por CalculadorVolumenes para hacer la suma de las áreas y la suma de volúmenes respectivamente. Al recibir un arreglo, la clase CalculadorSuma se vuelve ambigua pues podría recibir un arreglo generado por cualquier otra clase.

Aplicando herencia


¿Como solucionamos el problema anterior?
Para poder diferenciar la primera solución de la siguiente, agregaremos un sufijo PSL a las clases que iremos modificando, así en nuestro index.php podremos comparar los resultados.
Crearemos una nueva clase CalculadorVolumenesPSL que extienda de CalculadorAreaPSL.
Clase CalculadorVolumenesPSL
Recibe como parámetros objetos que extienden de la clase Figuras, calcula el volumen de cada uno y devolverá estos en un arreglo. Ademas, podemos pedir que la clase padre nos devuelva la suma de las áreas de los objetos llamando al método calcular() de la clase madre.
/* Clase CalculadorVolumenesPSL */
namespace Blockpc\Clases;
class CalculadorVolumenesPSL extends CalculadorAreaPSL {
private $figuras;
public function __construct(...$figuras) {
parent::__construct(...$figuras);
$this->figuras = $figuras;
}
public function calcular() {
foreach ($this->figuras as $figura) {
/* Validación del objeto */
if ( is_a($figura, 'Blockpc\Clases\Figuras') ) {
$volumenes[] = $figura->calcularArea() * $figura->getAltura() ?? 0;
continue;
}
throw new \Exception("Se esperaba una figura");
}
return $volumenes;
}
public function getVolumenes() {
return $this->calcular();
}
public function getSumaAreas() {
return parent::calcular();
}
}

Clase CalculadorAreaPSL
Recibe como parámetros objetos que extienden de la clase Figuras, calcula el área de cada uno y devolverá estas en un arreglo por medio del método calcular().
/* Clase CalculadorAreaPSL */
namespace Blockpc\Clases;
class CalculadorAreaPSL {
private $figuras;
public function __construct(...$figuras) {
$this->figuras = $figuras;
}
public function calcular() {
foreach ($this->figuras as $figura) {
/* Validación del objeto */
if ( is_a($figura, 'Blockpc\Clases\Figuras') ) {
$areas[] = $figura->calcularArea();
continue;
}
throw new \Exception("Se esperaba una figura");
}
return $areas;
}
}

Con estas dos clases podríamos calcular las áreas y los volúmenes, en el archivo index.php así:
/* Solo calculando las áreas */
$calculador = new Blockpc\Clases\CalculadorAreaPSL($circulo, $cuadrado, $triangulo, $rectangulo);
$areas = $calculador->calcular();
$sumaAreas = new Blockpc\Clases\CalculadorSuma($areas);
/* Calculando las áreas y los volúmenes */
$calculador = new Blockpc\Clases\CalculadorVolumenesPSL($circulo, $cuadrado, $triangulo, $rectangulo);
$volumenes = $calculador->getVolumenes();
$areas = $calculador->getSumaAreas();
$sumaVolumenes = new Blockpc\Clases\CalculadorSuma($volumenes);
$sumaAreas = new Blockpc\Clases\CalculadorSuma($areas);

Observaran que aplicando la herencia la clase CalculadorAreaPSL solo nos devuelve las áreas y que la clase CalculadorVolumenesPSL ahora también nos permite calcular volúmenes y áreas.
Aun usamos la clase CalculadorSuma para obtener la suma de los arreglos.

Aplicando el Principio de Sustitución de Liskov


Reescribiendo la clase CalculadorSumaPSL para que reciba como parámetro un objeto de CalculadorAreaPSL.
/* Clase CalculadorSumaPSL */
namespace Blockpc\Clases;
class CalculadorSumaPSL {
private $calculador;
public function __construct(CalculadorAreaPSL $calculador) {
$this->calculador = $calculador->calcular();
}
public function getSuma() {
return array_sum($this->calculador);
}
}

Es aquí donde se aplica el Principio de Sustitución de Liskov en cuanto el constructor pide un objeto CalculadorAreaPSL como parámetro.
Al ser este objeto un objeto padre, El principio nos dice que podríamos sustituir este objeto con cualquier objeto hijo, es decir que como parámetro podríamos usar un objeto de la clase CalculadorVolumenesPSL.
Así, en nuestro index.php
/* Solo calculando las áreas */
$areas= new Blockpc\Clases\CalculadorAreaPSL($circulo, $cuadrado, $triangulo, $rectangulo);
$sumaAreas = new Blockpc\Clases\CalculadorSumaPSL($areas);

En la primera linea, creamos un objeto CalculadorAreaPSL y se lo pasamos a la clase CalculadorSumaPSL. Ningún problema, era lo que se esperaba como parámetro.
Ahora bien...
/* Solo calculando los volúmenes */
$volumenes = new Blockpc\Clases\CalculadorVolumenesPSL($circulo, $cuadrado, $triangulo, $rectangulo);
$sumaVolumenes = new Blockpc\Clases\CalculadorSumaPSL($volumenes);

En la primera linea, creamos un objeto CalculadorVolumenesPSL y se lo pasamos a la clase CalculadorSumaPSL. Tampoco hay problema, pues se cumple el Principio de Sustitución de Liskov.

Ultimas consideraciones


La clase CalculadorVolumenesPSL es mas especifica (particular) que la clase CalculadorAreaPSL.
En la vida real, un objeto no puede tener volumen si no tiene un área o una altura, pero puede tener área aun no teniendo altura. Y es debido a esto que CalculadorVolumenesPSL extiende de CalculadorAreaPSL y no al revés.

El código fuente del articulo lo puedes descargar desde acá

Saludos y nos leemos en el próximo articulo.

Etiquetas solid

Ultima actualización Domingo 06 de Mayo, 2018




Agregar Comentario