Liskov Substitution Principle (LSP)
¿Qué es el Liskov Substitution Principle? ¿Cómo se aplica en React?
En pocas palabras lo que nos viene a decir el Principio de Substitución de Liskov (Liskov Substitution Principle, LSP) (la L dentro de los principio SOLID) es que un objeto que herede de una clase padre (un subtipo de objecto desde el punto de vista del padre en la jerarquía de herencia) debería poder ser sustituible por un objeto padre en dicha jerarquía de herencia.
¿Cónfunso, verdad? La respuesta es que sí, que así explicado como que cuesta verlo pero si lo explicamos con ejemplo es más que probable que las cosas queden mucho más claras. Pensemos por ejemplo en una clase que representa a un Animal
, una clase hija de la misma podría ser la clase Perro
. Bien, pues el LSP lo que nos viene a decir es que cualquier parte de nuestro código donde estuviésemos usando la clase Perro
deberíamos poder usar la clase Animal
sin que nuestro código se rompiese.
Esto que hemos explicado así de palabra lo podemos ver en el siguinte código donde tenemos una jerarquía de herencia en la que se presenta a la clase padre Animal
en la que se define el método swin
y dos clases hijas Dog
y Cat
que tienen su propia implementación de este método.
export class Animal {
swim(distance: number) {
console.log(`${distance} meters`)
}
}
export class Dog extends Animal {
swim(distance: number) {
console.log(`${distance} meters`)
}
}
export class Cat extends Animal {
swin(distance: number) {
new Error(`Cannot swin`)
}
}
Atendiendo a la definición del LSP veremos que que aquí se está rompiendo porque en principio la clase Dog
y la clase Cat
deberían poder ser intercambiables pero no es así, puesto que si llamamos al método swin
dentro de un objeto Cat
se produciría un error y por lo tanto nuestra aplicación se rompería.
const cat = new Cat()
cat.swin() // Se lanza un error.
Nota: este mismo ejemplo es replicable en el caso de una jerarquí de aves puesto que podríamos tener la clase padre a todas las aves
Ave
en la que se defina el métodovolar()
de tal manera que la mayoría de los pájaros que estén representando en las clases hijas podrían volar pero hay determinados tipos de aves (como lo pingüinos) que no vuelan y por lo tanto lanzarían un error cuando se llamase a su implementación del métodovolar()
.
¿Cómo podemos solucionar este problema y lograr que se cumpla el LSP? Pues la clave está en la propia jerarquía de herencia que hemos creado. Así deberíamos pensar en que en vez de tener una super clase genérica que englobe a todos los animales (la clase Animal
en el ejemplo anterior) podríamos a su vez tener una serie de subclases intermedias que nos ayuden a clasificarlos.
Esto quiere decir que si en nuestra jerarquía anterior ahora hacemos el siguiente refactor donde introducimos la clase intermedia SwinningAnimal
de la que deberían heredar todos los animales que efectivamente van a saber nadar dejándonos algo como lo siguiente:
export class Animal {}
export class SwimmingAnimal {
swim(distance: number) {
console.log(`${distance} meters`)
}
}
export class Dog extends SwimmingAnimal {
swim(distance: number) {
console.log(`${distance} meters`)
}
}
export class Cat extends Animal {}
LSP en React
El ejemplo que hemos usado es muy general y proviene de la programación orientada a objetos pero ¿cómo se puede ver el LSP en React? Pues vamos a partir de la definición de un componente muy simple que utilizaríamos para definir un botón en una aplicación:
const Button = ({ children, color, size }) => (
<button
style={{
color,
fontSize: size === 'xl' ? '32px' : '16px
}}
>
{children}
</button>
)
Ahora supongamos que tenemos un componente que vamos a usar para poder renderizar un botón de color rojo y cuya definición es la siguiente:
const RedButton = ({ children, isBig }) => (
<Button color="red" size={isBig ? 'xl' : 'sm'}>
){children}
</Button>
)
Como podemos ver en la implementación de RedButton
hace uso del componente Button
para lograr renderizar el color de botón rojo tal y como esperamos que suceda.
Ahora bien ¿cuál será uno de estos problemas en esta implementación y que estaría derivado de no estar siguiendo el LSP? Pues para entenderlo tenemos que pensar en la existencia de un tercer componente en el que estaríamos usando el componente RedButton
como sigue:
return <RedButton isBig={true}>Click me</RedButton>
Ahora imaginemos que a medida que va a evolucionando nuestra aplicación nos encontramos en la situación en la que queremos cambiar el botón rojo (el componente RedButton
) por el componente que a su vez utiliza este componente para componerse (es decir, que queremos cambiar el componente RedButton
por el componente Button
). Pues si se estuviese siguiendo el LSP simplemente deberíamos escribir lo siguiente:
return <Button isBig={true}>Click me</Button>
Y esto provocará un error puesto que el componente Button
no tiene la prop isBig
y por lo tanto se estará rompiendo el LSP porque aunque el componente RedButton
esté internamente utilizando el componente Button
en la implementación y utilización que se hace dentro del mismo le hemos cambiado el comportamiento de las props que van hacia abajo, ya que estamos usando la prop isBig
para determinar el valor que le asignaremos a la prop size
del componente Button
.
En este caso RedButton
no está herendado de Button
como se podría pensar en el caso de estar hablando de la programación orientada a objetos pero sí que podríamos decir que pero sí que podemos decir que desde el punto de vista del LSP un RedButton
que es un subtipo (esto entre muchas comillas) de otro tipo (el componente Button
) porque dentro de su código se estará haciendo uso del mismo (tenemos que tener en cuenta que Button
es el componente padre a partir del cual se está generando el RedButton
).
¿Cómo lo solucionaríamos? Pues simplemente deberíamos eliminar la prop isBig
del componente RedButton
y definir la prop size
garantizando además que los valores que pueda adoptar sean totalmente compatibles con los valores que espera recibir el componente Button
en su prop size
. Es decir, definiríamos:
const RedButton = ({ children, size }) => (
<Button color="red" size={size}>
){children}
</Button>
)
Y ahora podríamos hacer lo siguiente en el código del componente donde lo utilizaríamos:
return <RedButton size="xl">Click me</RedButton>
Esto nos va a permitir que el día de mañana, si fuese preciso, podríamos usar tanto el tipo RedButton
como el supertipo Button
en el mismo componente sin que en ningún momento se rompa nuestro código:
return <Button size="xl">Click me</Button>
Pero si nos fijamos en un detalle más aunque no existiría un cambio en el comportamiento de cómo se comportaría el botón sí que habría un cambio en la interfaz puesto que al haber cambiado RedButton
por Button
deberíamos añadir el color en que queremos que se renderice nuestro botón:
return (
<Button color="red" size="xl">
Click me
</Button>
)
Pero con esto estaremos evitando que al haber usado el subtipo RedButton
se esté rompiendo lo que podríamos hacer con el tipo Button
. Dicho de otra manera, no existe un cambio en el comportamiento a la hora de usar un componente padre y un componente hijo tal y como pide el LSP.
Resumen: En definitiva el LSP nos viene a decir que cualquier subtipo debería poder substituido por su tipo padre sin ningún problema lo que implica que el
RedButton
debería poder ser cambiado fácilmente por elButton
sin que se rompa absolutamente nada.