En el capítulo L, hablaremos del Principio de Sustitución de Liskov, una idea formulada por Barbara Liskov y Jeannette M. Wing, en su artículo A Behavioral Notion of Subtyping de 1994. Si quieres entender cómo formalizar los conceptos abstractos que manejamos, te recomiendo que lo leas.
Este principio está escrito con varias fórmulas que no entraremos a detallar, pero que de forma resumida dirían lo siguiente:
Dado un conjunto de clases que heredan de otra. Todas las clases hijas pueden ser sustituidas por la clase madre, y viceversa, sin requerir de cambios en el código que las usa.
En otras palabras, cualquier clase o sus subclases en una relación de herencia deberían ser intercambiables sin afectar la funcionalidad de la aplicación.
La violación del principio de sustitución de Liskov se puede detectar cuando las clases hijas contienen métodos vacíos, devuelven valores sin sentido o lanzan excepciones que indican que no están implementados.
Para entender mejor este principio, consideremos el ejemplo de una clase Vehículo que tiene las operaciones acelerar() y volar(). Si queremos usar estas operaciones en una función despegar(), deberíamos ser capaces de hacerlo con cualquier objeto de tipo Vehículo o sus subtipos, sin importar la clase específica del objeto.
classVehiculo {publicacelerar(): void { console.log("Estoy acelerando"); }publicvolar(): void { console.log("Estoy volando"); }}classCocheextendsVehiculo {publicacelerar(): void { console.log("Soy un coche que acelera"); }publicvolar(): void { thrownewError("Los coches no pueden volar"); } // ...de momento}functiondespegar(vehiculo: Vehiculo) {vehiculo.acelerar();vehiculo.volar();}constvehiculo: Vehiculo=newVehiculo();despegar(vehiculo);// Estoy acelerando// Estoy volandoconstcoche: Coche=newCoche();despegar(coche);// Soy un coche que acelera// Uncaught Error: Los coches no pueden volar
En el código anterior, podemos observar que si se recibe un objeto de tipo Coche en la función despegar(), se producirá una excepción y se romperá el flujo de ejecución. Esto significa que tendríamos que cambiar el código de despegar() para adaptarlo al comportamiento particular de la clase hija.
Sin embargo, este enfoque viola el principio de sustitución de Liskov, que es fundamental para mantener la flexibilidad y la extensibilidad del código. Es más, no solo estamos incumpliendo dicho principio, sino que también estamos violando el principio Abierto/Cerrado, que establece que un módulo debe estar abierto para su extensión pero cerrado para su modificación.
Para resolver este problema, debemos buscar un comportamiento común para todas las clases que se utilizan en la función despegar(). En el ejemplo dado, podemos crear una nueva clase hija llamada VehículoVolador que extienda de la clase Vehículo y que implemente el método volar(). De esta manera, podemos mantener la funcionalidad común en la clase Vehículo y agregar el comportamiento específico en las clases hijas.
classVehiculo {publicacelerar(): void { console.log("Estoy acelerando"); }}classVehiculoVoladorextendsVehiculo {publicvolar(): void { console.log("Estoy volando"); }}classAvionextendsVehiculoVolador {publicacelerar(): void { console.log("Soy un avión que acelera"); }publicvolar(): void { console.log("Libre al fin..."); }}functiondespegar(volador: VehiculoVolador ) {volador.acelerar();volador.volar();}constvolador: VehiculoVolador=newVehiculoVolador();despegar(volador);// Estoy acelerando// Estoy volandoconstavion: Avion=newAvion();despegar(avion);// Soy un avión que acelera// Libre al fin...
Ahora al añadir clases como: Dron, Aeroplano, Helicóptero, etc. Si hacemos que todas ellas hereden de VehículoVolador, ya no será necesario modificar el código de la función despegar().
Con esto terminamos el tercero de los principios SOLID, en el capítulo siguiente pasaremos a ver el Principio de Segregación de Interfaces. Y si queréis un resumen de todo esto, podéis verlo en Instagram.
En el capítulo L, hablaremos del Principio de Sustitución de Liskov, una idea formulada por Barbara Liskov y Jeannette M. Wing, en su artículo A Behavioral Notion of Subtyping de 1994. Si quieres entender cómo formalizar los conceptos abstractos que manejamos, te recomiendo que lo leas.
Este principio está escrito con varias fórmulas que no entraremos a detallar, pero que de forma resumida dirían lo siguiente:
En otras palabras, cualquier clase o sus subclases en una relación de herencia deberían ser intercambiables sin afectar la funcionalidad de la aplicación.
La violación del principio de sustitución de Liskov se puede detectar cuando las clases hijas contienen métodos vacíos, devuelven valores sin sentido o lanzan excepciones que indican que no están implementados.
Para entender mejor este principio, consideremos el ejemplo de una clase
Vehículo
que tiene las operacionesacelerar()
yvolar()
. Si queremos usar estas operaciones en una funcióndespegar()
, deberíamos ser capaces de hacerlo con cualquier objeto de tipoVehículo
o sus subtipos, sin importar la clase específica del objeto.En el código anterior, podemos observar que si se recibe un objeto de tipo
Coche
en la funcióndespegar()
, se producirá una excepción y se romperá el flujo de ejecución. Esto significa que tendríamos que cambiar el código dedespegar()
para adaptarlo al comportamiento particular de la clase hija.Sin embargo, este enfoque viola el principio de sustitución de Liskov, que es fundamental para mantener la flexibilidad y la extensibilidad del código. Es más, no solo estamos incumpliendo dicho principio, sino que también estamos violando el principio Abierto/Cerrado, que establece que un módulo debe estar abierto para su extensión pero cerrado para su modificación.
Para resolver este problema, debemos buscar un comportamiento común para todas las clases que se utilizan en la función
despegar()
. En el ejemplo dado, podemos crear una nueva clase hija llamadaVehículoVolador
que extienda de la claseVehículo
y que implemente el métodovolar()
. De esta manera, podemos mantener la funcionalidad común en la claseVehículo
y agregar el comportamiento específico en las clases hijas.Ahora al añadir clases como: Dron, Aeroplano, Helicóptero, etc. Si hacemos que todas ellas hereden de
VehículoVolador
, ya no será necesario modificar el código de la función
.despegar()
Con esto terminamos el tercero de los principios SOLID, en el capítulo siguiente pasaremos a ver el Principio de Segregación de Interfaces. Y si queréis un resumen de todo esto, podéis verlo en Instagram.
Más artículos de esta serie:
Capítulo S: Single Responsibility Principle
Capítulo O: Open/Closed Principle
Capítulo I: Interface-Segregation Principle
Capítulo D: Dependency Inversion Principle
Si quieres leer algo más de Barbara Liskov, tiene un libro muy completo para aquellas personas que programan en Java: Program Development in Java: Abstraction, Specification, and Object-Oriented Design.
jose