DEV Community

Cover image for El Principio de Sustitución de Liskov: Más allá de la herencia
diek
diek

Posted on

El Principio de Sustitución de Liskov: Más allá de la herencia

El otro día, mi amiga Laura me contó una situación que le pasó en su trabajo y me hizo reflexionar sobre cómo explicamos los conceptos de programación. Resulta que estaba en una reunión de equipo discutiendo sobre la refactorización de un sistema legacy, y cuando mencionó el Principio de Sustitución de Liskov, uno de los desarrolladores junior preguntó: "¿Eso no es simplemente que las subclases deben poder usarse en lugar de sus clases base?". Laura se dio cuenta de que, aunque técnicamente no estaba equivocado, esa simplificación estaba llevando al equipo a malentender y aplicar incorrectamente el principio.

Esta anécdota me recordó que a veces, en nuestro afán por simplificar, podemos perder la esencia de conceptos importantes. Así que hoy vamos a sumergirnos en el Principio de Sustitución de Liskov (LSP), la "L" de SOLID, y vamos a ver por qué es mucho más que "las subclases deben poder usarse en lugar de sus clases base".

¿Qué es realmente el Principio de Sustitución de Liskov?

Imagina que tienes un coche. Sabes conducirlo, conoces sus controles, sabes qué esperar cuando pisas el acelerador o el freno. Ahora, supongamos que te dan un coche nuevo, supuestamente una versión mejorada del anterior. Pero resulta que cuando pulsas el freno, el coche acelera. ¿Te sentirías seguro conduciendo ese coche? Probablemente no.

Pues bien, el Principio de Sustitución de Liskov viene a decirnos algo similar en el mundo del software: si S es un subtipo de T, entonces los objetos de tipo T en un programa pueden ser reemplazados por objetos de tipo S sin alterar ninguna de las propiedades deseables de ese programa.

En otras palabras, no solo se trata de que una subclase pueda usarse en lugar de su clase base, sino que debe comportarse de una manera que no sorprenda a quien esté usando el programa.

¿Por qué es importante?

Imagina que tienes una clase Ave con un método volar(). Luego creas una subclase Pingüino que hereda de Ave. Técnicamente, un pingüino es un ave, así que parece tener sentido, ¿verdad? Pero aquí está el problema: los pingüinos no vuelan.

class Ave:
    def volar(self):
        print("Volando alto")

class Pingüino(Ave):
    def volar(self):
        raise Exception("¡Socorro! ¡Este pingüino no puede volar!")
Enter fullscreen mode Exit fullscreen mode

Si en alguna parte de tu código estás esperando poder llamar al método volar() de cualquier Ave, el Pingüino va a causar problemas. Esto viola el Principio de Sustitución de Liskov porque no puedes usar un Pingüino en cualquier lugar donde se espere un Ave sin que el programa falle.

Cómo aplicar el LSP

Vamos a ver cómo podríamos mejorar nuestro ejemplo anterior:

class Animal:
    def mover(self):
        pass

class AveVoladora(Animal):
    def mover(self):
        print("Volando alto")

class AveNoVoladora(Animal):
    def mover(self):
        print("Caminando")

class Gorrion(AveVoladora):
    pass

class Pingüino(AveNoVoladora):
    pass
Enter fullscreen mode Exit fullscreen mode

Ahora, cualquier parte del código que espere un Animal puede trabajar con Gorrion o Pingüino sin problemas. Hemos respetado la expectativa de comportamiento.

Beneficios del LSP

  1. Código más robusto: Menos sorpresas desagradables en tiempo de ejecución.

  2. Mejor diseño de jerarquías: Te obliga a pensar cuidadosamente en las relaciones entre clases.

  3. Facilita el polimorfismo: Puedes trabajar con abstracciones de alto nivel con confianza.

  4. Mejora la reusabilidad: Las clases que respetan LSP son más fáciles de usar en diferentes contextos.

  5. Facilita el testing: Puedes escribir pruebas para la clase base y estar seguro de que funcionarán para las subclases.

Más allá de la herencia

Es importante entender que el LSP no se limita solo a la herencia de clases. También se aplica a interfaces y, en un sentido más amplio, a cualquier tipo de sustitución en tu código.

Por ejemplo, si tienes una función que espera un cierto tipo de objeto, cualquier objeto que pases a esa función debería comportarse de manera consistente con lo que la función espera, independientemente de su tipo concreto.

En la práctica

Aplicar el LSP puede ser desafiante, especialmente cuando estamos lidiando con sistemas complejos. Aquí hay algunas preguntas que puedes hacerte:

  1. ¿Las subclases que estoy creando respetan los contratos (precondiciones y postcondiciones) de la clase base?
  2. ¿Estoy fortaleciendo las precondiciones o debilitando las postcondiciones en las subclases?
  3. ¿El comportamiento de mis subclases sería sorprendente para alguien que solo conoce la interfaz de la clase base?

Si alguna de estas preguntas te hace dudar, es posible que estés violando el LSP.

Conclusión

El Principio de Sustitución de Liskov es mucho más que una regla sobre herencia. Es una guía para diseñar jerarquías de clases e interfaces que sean coherentes y predecibles. No se trata solo de que el código compile, sino de que se comporte de manera lógica y esperada.

La próxima vez que estés diseñando tus clases, piensa en Laura y su equipo. No te quedes solo con la definición superficial. Pregúntate: "¿Estoy creando subtipos que realmente pueden sustituir a sus tipos base sin causar sorpresas? ¿Estoy respetando las expectativas de comportamiento?".

Recuerda, en programación como en la vida, la confianza es fundamental. Un código que respeta el LSP es un código en el que puedes confiar, un código que no te va a dar sustos inesperados en producción. Y un programador que entiende y aplica el LSP es un programador en el que su equipo puede confiar, aunque a veces se le olvide la definición exacta de la "L" de SOLID.

Top comments (1)

Collapse
 
_firelinks profile image
Mike Dabydeen

¡Excelente explicación del Principio de Sustitución de Liskov! Has capturado perfectamente la esencia de este principio, más allá de la simple herencia. Me encanta cómo has ejemplificado el concepto con el caso del pingüino, ya que realmente subraya la importancia de mantener comportamientos esperados y coherentes en nuestras jerarquías de clases. Es un recordatorio clave de que un buen diseño de software no solo se trata de hacer que el código compile, sino de garantizar que sea lógico, predecible y confiable. Sin duda, reflexionar sobre estas ideas mejora nuestra habilidad para crear sistemas más robustos y mantenibles. ¡Gran post!