Hamburger Icon
Repaso a los principios SOLID: (L) Principio de Sustitución de Liskov

Repaso a los principios SOLID: (L) Principio de Sustitución de Liskov

Imagina un robot azul, con forma de gato. Sin orejas. Ah, y puede viajar en el tiempo. Si no estás pensando en Doraemon y eres de los 80-90, no tienes infancia, lo siento. Doraemon viajó desde el siglo XXII para cuidar de Nobita. Es capaz de sentir emociones y razonar como cualquier ser humano -vaya, una IA de verdad- y su principal herramienta de trabajo es su bolsillo mágico, donde puede encontrar de todo. Además, Doraemon es un crack del LSP.

Los principios SOLID son un conjunto de cinco principios de diseño de software que fueron propuestos por el programador Robert (tío Bob) Martin. Estos principios son guías que ayudan a desarrollar software que sea más mantenible, flexible y fácil de entender. Cada letra de la palabra "SOLID" representa uno de estos principios, en este post vamos a ver en qué consiste la L.

Antes de la L viene la O, por si te la saltaste.

LSP (Liskov Substitution Principle)

Principio de sustitución de Liskov. Ahora veremos en qué se refiere esa sustitución, pero antes de empezar, es necesario mencionar a Barbara Liskov, quien propuso inicialmente este principio que después fue popularizado por el gran Robert C. Martin.

Definición

El LSP es uno de los fundamentos de la programación orientada a objetos y establece que los objetos de una subclase deben ser sustituibles por objetos de su clase base sin afectar la integridad del programa.

Dicho de otro modo, si tienes una clase base y una subclase que hereda de ella, cualquier instancia de la subclase debe ser capaz de ser utilizada en lugar de una instancia de la clase base sin cambiar el comportamiento esperado del programa.

Imagina que Doraemon es una clase base que tiene una serie de herramientas mágicas, como el gorrocóptero, la puerta mágica o la máquina del tiempo. Estas herramientas tienen propiedades y comportamientos específicos que deben cumplir con un contrato establecido por Doraemon. Si eres su amigo, tendrás acceso a ellas.

Luego, imagina que Nobita, Shizuka, Suneo y Gigante son subclases de Doreamon, y cada uno de ellos debe seguir las reglas y expectativas impuestas por Doraemon cuando usen sus herramientas mágicas.

Si una subclase no puede reemplazar perfectamente a su clase base sin causar problemas, esto puede ser indicativo de un mal diseño de clases. Veamos un ejemplo en código para ilustrar esto.

Siguiendo la jerarquía de clases expuesta hace un momento con Doraemon, tenemos una clase base (Doreamon) y un par de subclases (Nobita y Shizuka). Nobita, que es muy pillo cuando quiere, decide implementar por su cuenta un método para espiar a Shizuka mientras se baña:

class Doraemon:
    def puertaMagica(self):
        pass

class Shizuka(Doraemon):
    def puertaMagica(self):
        # implementa este método para llegar a tiempo a sus clases de música
        pass

class Nobita(Doraemon):
    def puertaMagica(self):
        # implementa este método para llegar a tiempo a la escuela
        pass

    def voyeurMagico(self):
        # se apoya de la puerta mágica para espiar a Shizuka mientras se da un baño
        pass

La subclase Nobita está violando el LSP -y de paso la privacidad de su amiga- al agregar el método voyeurMagico() que no es coherente con la clase base según las normas impuestas por Doraemon. Una posible opción para seguir el Principio de sustitución de Liskov sería crear una interfaz adicional para manejar casos específicos:

class Doraemon:
    def puertaMagica(self):
        pass

class Shizuka(Doraemon):
    def puertaMagica(self):
        # implementa este método para llegar a tiempo a sus clases de música
        pass

class Pervertido(self):
    def voyeurMagico(self):
        pass

class Nobita(Doraemon, Pervertido):
    def puertaMagica(self):
        # implementa este método para llegar a tiempo a la escuela
        pass

    def voyeurMagico(self):
        # se apoya de la puerta mágica para espiar a Shizuka mientras se da un baño
        pass

La interfaz Pervertido es la que maneja el comportamiento de voyeur. Esto permitiría que todos los pervertidos espiaran a quien quisieran implementando el método voyeurMagico() de manera coherente, aunque no moral. Es un fallo ético, pero cumple con el LSP ya que todas las subclases aún pueden ser sustituibles por sus clases base.

Beneficios

Flexibilidad y reusabilidad: el LSP permite la creación de clases heredadas que se pueden reutilizar y extender sin preocuparse por efectos secundarios inesperados en el código existente.

Interoperabilidad: facilita el intercambio y la colaboración entre clases, ya que las subclases pueden funcionar de manera coherente con las clases base en diferentes contextos.

Aplicación

Para aplicar correctamente este principio puedes considerar varios aspectos y pautas clave.

Relación "es un": asegúrate de que la relación "es un" entre una subclase y su clase base sea válida y coherente. Una subclase debe ser una extensión lógica de su clase base.

Mismo contrato: las subclases deben adherirse al mismo contrato de comportamiento que su clase base. En otras palabras, que los métodos y propiedades definidos en la clase base deben ser implementados en las subclases de manera coherente y consistente.

Precondiciones y postcondiciones: revisa que las precondiciones -esto es, condiciones que deben cumplirse antes de llamar a un método- y las postcondiciones -resultados esperados después de llamar a un método- se mantengan en las subclases. Tenerlo en cuenta ayuda a garantizar que las subclases no violen el contrato de la clase base.

Evita comportamientos imprevistos: las subclases no deben introducir comportamientos no razonables en el contexto de la clase base. Si una subclase cambia significativamente el comportamiento de un método heredado, podría llevar a problemas al reemplazar instancias de la clase base con instancias de la subclase.

Pruebas unitarias: realiza pruebas exhaustivas para garantizar que las subclases se comporten correctamente y cumplan con el contrato establecido por la clase base. Las pruebas unitarias ayudarán a identificar posibles violaciones del LSP.

Conclusiones

Cumplir con el principio de Sustitución de Liskov ayuda a garantizar la integridad del diseño orientado a objetos y a crear jerarquías de clases sólidas y coherentes. Al seguir este principio, puedes crear un código más predecible y confiable que sea más fácil de mantener y evolucionar con el tiempo.

Aunque Nobita se aproveche del buenismo de Doraemon, cuando este se entere de que está usando una interfaz para aprovechar la puerta mágica a escondidas, le va a cerrar el grifo immediatamente. Así que ya lo sabes, cuando crees subclases, asegúrate de cumplir con el contrato y las expectativas de la clase heredada, no te saques de la manga métodos que perviertan el código.

Puedes continuar con el ISP.