Principio de sustitución de Liskov (LSP)

Corría el año 1987 cuando Barbara Liskov, en una conferencia magistral titulada La Abstracción de Datos y Jerarquía, hablaba por primera vez de tipificación fuerte, haciendo referencia a la herencia entre objetos.

Algunos años después (en 1994), junto con Jeanette Wing, finalmente le daba forma a lo que hoy conocemos como Principio de sustitución de Liskov (LSP), el cual representa la L dentro de los 5 principios SOLID.

El principio, tal cual lo enunciaron Liskov y Wing es el siguiente:

Sea ϕ(x) una propiedad comprobable acerca de los objetos x de tipo T. Entonces ϕ(y) debe ser verdad para los objetos y del tipo S donde S, es un subtipo de T.

Dicho en otras palabras:

Si una clase C es extendida por una clase H, debemos de ser capaces de sustituir cualquier objeto de C por cualquier objeto de H sin que el programa deje de funcionar o se den comportamientos inesperados.

O quizás quede más claro así:

Este principio refuerza el concepto de diseño por contratos propuesto por Bertrand Meyer y que a día de hoy, creo que todos conocemos muy bien.


¿Cómo cumplimos con el LSP?

Veamos un ejemplo de código que no cumple con el LSP:

Matemáticamente hablando, un cuadrado es igual que un rectángulo, por lo tanto que nuestra clase Square herede de Rectangle puede parecernos totalmente lógico.

Ahora bien, que sucede con los métodos setWidth($width) y setHeight($height)? En el caso del cuadrado el ancho es igual que el alto, así que, como en el ejemplo, podemos solucionar el problema haciendo que cada vez que se llame a uno de estos métodos, fije por igual el valor del alto y del ancho.

A priori puede parecer una solución válida, pero veamos un ejemplo de cómo podría fallar esto:

public function testAreaIsBeingCalculated(Rectangle $rectangle)
{
$rectangle->setHeight(6);
$rectangle->setWidth(4);
    $this->assertEquals(24, $rectangle->area());
}

En el caso de que estemos pasando un objeto Rectangle puro, todo irá como es de esperar, pero como le pasemos un Square nos va a devolver un error, ya que el area será 16 y no los 24 que esperamos.

El problema reside en que ambos métodos setHeight y setWidth siguen siendo públicos, ya que vienen heredados de Rectangle donde sí tienen sentido, y por lo tanto de cara al desarrollador esos métodos pueden ser usados.

Otra mala solución sería forzar la clase Square a que lance una excepción con alguno de los dos métodos ( setWidth o setHeight, pero no en ambos, o perderíamos toda la funcionalidad). Y digo mala solución porque nadie esperaría este resultado de un método tan básico de la clase Rectangle que en la propia clase base no lanza ninguna excepción.

Recordemos: los métodos sobrescritos deben de conservar el comportamiento original, si el método original no lanzaba excepciones o no permitía devolver null, el método sobrescrito tampoco debería.

En este caso la solución pasa por hacer nuestra jerarquía de clases más especifica, sacando a una clase superior los rasgos comunes y dejando a cada clase especifica que modele sus diferencias:

¿Cómo detectamos que no cumplimos con el LSP?

O dicho de otra manera, a qué huele una clase que no cumple con el LSP?

La forma más sencilla de detectar el incumplimiento del LSP es observando las clases que extienden de otras clases. Si estas poseen métodos sobrescritos que devuelven null o lanzan excepciones sabiendas de que la clase de la que heredan no lo hace, estamos ante una clara violación del LSP.

Si por otra parte estamos heredando de clases abstractas que nos obligan también a lanzar excepciones o retornar null, también estamos ante un caso de violación del LSP.

Conclusión

Aunque a priori puede parecer trivial, el LSP no hay que tomarlo a la ligera, la jerarquía de clases es el cimiento sobre el que se asienta toda nuestra aplicación, y un fallo de diseño a ese nivel puede suponer un desastre difícil de corregir cuando esta haya crecido. Ahora que tenemos claras las señales de alerta y sabemos como evitar el error, evitemos a toda costa violar el LSP ;)

Hasta la próxima!