El Principio de Sustitución de Liskov

En 1987, mientras daba una conferencia magistral sobre abstracciones de datos y jerarquías, Barbara Liskov introdujo la idea que eventualmente se convertiría en el principio de sustitución de Liskov. La siguiente es una descripción moderna (y muy formal) del principio:

Sea Φ(x) una propiedad demostrable sobre objetos x de tipo T. Entonces Φ(y) debería ser verdadera para objetos y de tipo S donde S es un subtipo de T.

Cuando leí la definición por primera vez, lo único que entendí fue que está de alguna manera relacionada con herencia… ¿tal vez? Se me ocurrió una frase más simple (aunque menos completa) que podría ser más fácil de entender:

Un trozo de código que depende de un objeto de tipo P debería ser capaz de operar apropiadamente con objetos de tipo C, donde C es un subtipo de P

Lo que esto significa es que deberíamos diseñar nuestras abstracciones y clases de una manera que facilite la interoperabilidad a través de la jerarquía de herencia completa. Si una subclase sobreescribe el comportamiento de su ancestro y rompe la compatibilidad con el resto del código, estamos violando el principio.

Entender el LSP es mucho más fácil con un ejemplo. Echemos un vistazo al problema Cuadrado-Rectángulo propuesto por Robert C. Martin.

Cosas que funcionan en español podrían no funcionar en código

Todos estamos de acuerdo con la noción de que ‘un cuadrado es un rectángulo’. Esta afirmación es por definición correcta, pero como muchas otras cosas, podría no funcionar tan bien en el mundo de la programación.

Supón que tenemos el siguiente código donde afirmamos que la función get_area de Rectangle se comporta apropiadamente:

rectangle = #... Obtiene un objeto Rectangle de algún lugar, tal vez una factory
rectangle.set_height(5)
rectangle.set_width(4)
raise "El cálculo del área está mal" unless rectangle.get_area == 20
#... más código

# En otro archivo, puedes encontrar una implementación hipotética de la clase Rectangle
class Rectangle
    def initialize()
    end

    def set_width(width)
        @width = width
    end

    def set_height(height)
        @height = height
    end

    def get_area
        return @width * @height
    end
end

Este código lanzará una excepción a menos que el cálculo de get_area devuelva el valor apropiado.

Ahora, imagina que implementamos una clase Square, que también es por definición un Rectangle. No puedes establecer el ancho y alto de un cuadrado independientemente, esos dos valores siempre son iguales para un cuadrado.

Además de la interfaz pública de Rectangle, Square implementa el método set_side.


rectangle = #... Esta línea ahora da un objeto Square
# El resto del código se mantiene igual

# En otro archivo, puedes encontrar una implementación hipotética de la clase Square
class Square < Rectangle
    def set_width(width)
        set_side(width)
    end

    def set_height(height)
        set_side(height)
    end

    def set_side(side_length)
        @height = side_length
        @width = side_length
    end

end

Si ejecutamos este código, la afirmación fallará, ya que el área ahora es 16. El código que funcionaba con objetos de tipo Rectangle (y sus subtipos) se rompe con Square, incluso si implementa la misma interfaz pública y se comporta como un Square debería.

LSP se trata de semántica

El LSP se trata de consistencia semántica en una jerarquía de tipos. No es suficiente implementar la misma interfaz pública a través de dichas jerarquías, el comportamiento también debería ser consistente.

Aspiramos a la interoperabilidad, y la habilidad de trabajar con subtipos sin la necesidad de implementar ‘manejo especial’ para casos atípicos.

En nuestro ejemplo, podríamos estar tentados a resolver el problema con una declaración if basada en tipos: si encontramos un objeto de tipo Square, realizar una acción X diferente. Este es un problema que puede espiralizarse fuera de control muy fácilmente y contaminar pedazos de código que dependen de Rectangle mientras nuestro código crece.

Lo que necesitamos en su lugar es un enfoque disciplinado: cada tipo que implementa la interfaz pública de Rectangle debería actuar como un Rectangle mientras estemos usando la misma interfaz pública.

Este principio es acerca de más que las jerarquías de clases tradicionales en lenguajes estáticamente tipados. También se usa cuando se comparte una interfaz pública con duck typing, o diseñando microservicios con una interfaz REST compartida.

¡Genial! Ahora también sabes sobre la L de los principios S.O.L.I.D. El conocimiento de este tema es importante para tomar las decisiones arquitectónicas correctas. Espero que puedas usarlo en tus proyectos futuros.

Qué hacer a continuación

  • Comparte este artículo con amigos y colegas. Gracias por ayudarme a llegar a gente que podría encontrar útil esta información.
  • Lee el artículo sobre el LSP de Barbara Liskov.
  • Hay más información sobre el OCP en el capítulo 9 de Clean Architecture.
  • Envíame un email con preguntas, comentarios o sugerencias (está en la página Autor).

Juan Luis Orozco Villalobos

¡Hola! Soy Juan, ingeniero de software y consultor en Budapest. Me especializo en computación en la nube e IA, y me encanta ayudar a otros a aprender sobre tecnología e ingeniería