Manual de buenas prácticas: L de S.O.L.I.D. Sustitución de Liskov
Continuamos con nuestra serie de principios S.O.L.I.D. y es el turno del principio sustitución de Liskov.
Os dejo por aquí los enlaces a los anteriores principios por si os apetece adentraros en más buenas prácticas de programación:
- S de S.O.L.I.D. Responsabilidad única
- O de S.O.L.I.D. Abierto / cerrado
- I de S.O.L.I.D. Segregación de la interfaz
- D de S.O.L.I.D. Inversión de dependencia
Curioso que este principio tenga un nombre propio, ¿verdad?
¿Quién se gana tanto mérito como para que su nombre sea uno de los más mencionados dentro del mundo de las buenas prácticas de programación?
Pues este es el caso de Bárbara Liskov , que en una conferencia allá por los años 80 plasmó por primera vez la solución a un caso muy particular en arquitectura de programación orientada a objetos.
Pero Juanjo, ¿no os vas a decir lo que Bárbara dijo exactamente?
No. Lo siento. Ese párrafo es ininteligible. Déjame ahorrarte el ‘estrujamiento’ de cerebro.
Para los más mundanos viene a decir que:
Cuando una clase hereda de otra, no debería importar si usas la clase padre o la heredada, el código resultado debería ser el mismo. Si la lógica de la aplicación nos obliga a utilizar un condicional o lanzar una excepción en una de las clases…¡algo va mal!
Bueno, vamos a lío, ¿no? Aunque la cosa hoy no vaya de ruedas, si que va de coches.
Siento utilizar la más que trillada clase ‘coche’, pero creo que en este caso es muy didáctico para entender este principio que personalmente me pareció difícil de asimilar.
El principio de sustitución de Liskov mal implementado ¶
Una clase coche muy sencillita para poder asimilar bien el concepto. Dos métodos: país de procedencia y tipo de combustible.
Vamos Juanjo, atrévete con algo que no sea PHP.
Vamos allá. Lo siento por las meteduras de pata, amantes de JavaScript:
class Car{
getCountry(){}
getFuel(){}
}
Vamos a crear una clase que herede de Car
y va a ser para un coche de gasolina de la marca Renault:
class RenaultPetrol extends Car{
getCountry(){
return 'Francia';
}
getFuel(){
return 'Gasolina';
}
}
Hasta aquí vamos bien, pero vamos crear una clase que herede de Car
para un coche que no funcione con combustible, sino con electricidad:
class Tesla extends Car{
getCountry(){
return 'United States';
}
getFuel(){
throw new Error('Los coches eléctricos no tienen combustible, funcionan con electricidad');
}
}
Pero qué pasa ahora si en el código de nuestra aplicación, el que implementa la instanciación de estas clases, intentamos escribir una función que obtenga el tipo de combustible y lo ejecutamos contra las dos definiciones que hemos declarado antes?
Vamos a verlo:
function fuelType(car){
car.getFuel()
}
const renaultPetrol = new RenaultPetrol();
const tesla = new Tesla();
fuelType(renaultPetrol);
//devuelve 'Gasolina'
fuelType(tesla);
//devuelve Excepcion: 'Los coches eléctricos no tienen combustible, funcionan con electricidad'
Tal y como podemos ver con la instanciación de la clase RenaultPetrol
, el principio de Liskov se implementa correctamente, ya que la clase que hereda del padre, funciona tal y como el padre prevé:
La clase padre Car
, espera que su clase heredada RenaultPetrol
funcione exactamente igual que ella: da igual si utilizas el padre o el hijo, cabe esperar que el método getFuel()
obtenga el tipo de combustible y no que lance una excepción que diga: no funciono con combustible. Pero esto si que pasa con la ejecución del método contra la instanciación de la clase Tesla
.
El principio de sustitución de Liskov implementado correctamente ¶
Ahora viene la cuestión clave:
¿Qué tenemos que incluir en el código anterior para que estemos seguros de que no importe el tipo de coche que declaremos, no vamos a tener una excepción?
La solución más sencilla es implementar dos clases intermedias entre Car
y sus clases hijas para así tener en cuenta los dos tipos de coches: los eléctricos y los que funcionan con combustible.
Nuestra clase Car
implementará el método que hasta ahora es común a todas las clases que heredan: getCountry()
.
class Car{
getCountry(){}
}
Ahora creamos dos clases intermedias, una para coches de combustible y otra para coches eléctricos:
class FuelCar extends Car{
getFuel(){}
}
class ElectricCar extends Car{
getEnergyUSed(){
return 'Electricidad'
}
}
Y nuestras clases que heredan, ya no heredan de Car
, sino que heredarán de estas nuevas clases intermedias:
class RenaultPetrol extends FuelCar{
getCountry(){
return 'Francia';
}
getFuel(){
return 'Gasolina';
}
}
class Tesla extends ElectricCar{
getCountry(){
return 'United States';
}
}
Si os dais cuenta, la clase Tesla
ya no necesita tener un método propio que especifique el tipo de energía que usa, ya que su padre solo puede tener un tipo de energía, la eléctrica.
En el caso de los coches que usan combustible, pueden ser de varios tipos: diésel y gasolina (por ejemplo), así que la clase que extiende tendrá por si sola que especificar que es del tipo gasolina en su método getFuel()
.
Ahora el código de nuestra aplicación se vería así:
function fuelType(fuelCar){
fuelCar.getFuel()
}
function energyUsed(ElectricCar){
ElectricCar.getEnergyUSed()
}
const renaultPetrol = new RenaultPetrol();
const tesla = new Tesla();
fuelType(renaultPetrol);
//devuelve 'Gasolina'
energyUsed(tesla);
//devuelve 'Electricidad'
¿En ambos casos podríamos utilizar tanto la clase heredada como la clase padre sin que el código lance una excepción inesperada?
Sí.
¿Cumplimos el principio Liskov?
Sí.
¿Te ha costado asimilar el concepto? ¶
Puede ser que el principio sea complicado de entender en principio viéndolo en un ejemplo, pero tal y como deberíamos hacer con todos los principios de código limpio, es un concepto que tenemos que interiorizar.
Mi consejo como siempre es que empecéis a programar una aplicación y que falléis.
Aceptad tantas excepciones como podáis en las primeras fases del código, es normal que las tengáis.
Colocad tantos if
y lanzad tantas excepciones como podáis, pero después no olvidéis dar otra vuelta de tuerca para implementar tantas buenas prácticas como podáis. Pero no todo de golpe. Id cambiando pequeñas porciones de código porque la implementación de buenas prácticas es totalmente escalable e id interiorizando conceptos poco a poco.
Para indagar más en los conceptos S.O.L.I.D. y tener otros ejemplos que os ayuden a entenderlos mejor, os invito que escuchéis el episodio de Web Reactiva sobre los Principios S.O.L.lD..
Por otro lado, os dejo también el código de esta aplicación para que la probéis en una aplicación sencillita y mostrando las salidas por navegador, la podéis encontrar en el repositorio de buenas prácticas de GitHub.
Os mando un fuerte abrazo, amigos programadores, y recordad:
¡Nos vemos en el siguiente fragmento de conocimiento, equipo!
Escrito originalmente por: Juan José Ramos
Daniel Primo
12 recursos para developers cada domingo en tu bandeja de entrada
Además de una skill práctica bien explicada, trucos para mejorar tu futuro profesional y una pizquita de humor útil para el resto de la semana. Gratis.