Principio de sustitución de Liskov (LSP) en Swift

Genesis Sanguino
Swift en español
Published in
4 min readApr 8, 2021

Principio de sustitución de Liskov (LSP)

Este principio recibe el nombre de la persona que lo acuñó: Barbara Liskov quien dio su definición original y matemática:

“Si para cada objeto o1 de tipo S hay un objeto o2 de tipo T, tal que para todos los programas P definidos en términos de T, el comportamiento de P no cambia cuando o1 se sustituye por o2, entonces S es un subtipo de T. “

Puede ser más fácil de entender, especialmente para las personas que están estudiando estos principios por primera vez, si no nos fijamos en esa definición matemática.

En palabras más simples este principio trata sobre que los objetos de un desarrollo, deberían ser reemplazables por instancias de sus subtipos sin alterar el correcto funcionamiento del mismo. Dicho de otro modo: cualquier subclase debería poder ser sustituible por la clase padre.

Recordando en el artículo anterior de esta serie hablamos sobre el Principio Open-Closed (OCP), que básicamente dice que las entidades de software deben estar abiertas para extensión pero cerradas para modificaciones, lo que hace que el código sea mantenible y reutilizable. Además para obtener esos resultados debemos desarrollar nuestro código utilizando abstracciones como herencia e interfaces. En este artículo nos centraremos en las interfaces para presentar los beneficios de respetar el LSP y también mostraremos que cuando no seguimos el LSP a menudo también rompemos el OCP, es por eso que estos principios están relacionados.

Ejemplos en Swift

Consideremos este ejemplo donde tenemos una superclase Animal y dos subclases, Dog y Cat. Básicamente en este ejemplo la clase Animal funciona de manera similar a una implementación de clase abstracta.

Además tenemos un método de printInfo(animal:), donde consumimos esas clases y sus métodos específicos. En este método recibimos un objeto de tipo Animal y tratamos de convertirlo como cada uno de nuestros tipos de subclases, para luego poder imprimir su respectiva raza.

En este ejemplo estamos rompiendo el LSP porque un objeto del tipo de la subclase Dog se comporta de manera diferente a un objeto del tipo de la superclase Animal como parámetro de este método. Si el método recibe al hijo va a imprimir correctamente la información deseada, pero si recibiera al padre el método simplemente no haría nada. La misma diferencia ocurre si el método recibe objetos del tipo de la subclase Cat, en comparación con un objeto de tipo de la superclase Animal.

Además es importante señalar que en este ejemplo también estamos rompiendo el OCP, por que el método no está cerrado para modificaciones si decidimos extender nuestro tipos de animales. Por ejemplo, si decidimos crear una nueva subclase Bird tendríamos que agregar una instrucción if adicional en ese método para poder obtener su información.

¿Cómo podemos respetar el LSP?

Una forma de hacer que nuestro ejemplo respete el LSP es crear un protocolo con un método común printBreed().

Esto evitaría tener comportamientos diferentes para un objeto de tipo superclase y un objeto de tipo subclase, resolviendo la ruptura del LSP.

Adicionalmente esto haría que el método printInfo(animal:) quede cerrado para modificaciones al extender nuevos animales, porque el nuevo animal tendría que implementar el protocolo por lo que debería tener el método printBreed().

✔️¿Cuáles son los principales beneficios de respetar el LSP?

  • Primero y más importante: asegura que estemos haciendo bien nuestras abstracciones. El objetivo de poder crear una clase basada en una clase abstracta es que pueda usar la clase derivada sin problemas, sin tener que tocar ninguna de las otras clases derivadas y sin romper nuestra compilación.
  • Segunda razón: es una herramienta excepcional para detectar diseños deficientes. Por ejemplo, si tenemos una abstracción y una de sus clases derivadas introduce un campo o método que requiere un tratamiento especial (los famosos “if else”), probablemente estamos violando el LSP.
  • Tercera razón: nuestro código de tests unitarios y de integración para la clase base son completamente reutilizables para nuestras clases derivadas. Esto es muy relevante ya que en cualquier caso las pruebas unitarias son algo que consume tiempo, por lo que si podemos reutilizar ese código podemos ahorrar mucho tiempo!.
  • Cuarta razón: si estamos haciendo algún tipo de comparación o prueba de componentes de software que deben tener el mismo comportamiento, nos permite reemplazar fácilmente estos componentes.

En resumidas cuentas el mayor beneficio que obtenemos al seguir el principio de sustitución de Liskov es código que es altamente flexible, que se puede probar y que se adhiere al OCP.

Conclusión

Como desarrolladores no queremos saber todo lo que otros desarrolladores están implementando y definitivamente no debemos hacerlo, por lo que aquí está la importancia de tener excelentes interfaces (con excelentes nombres). Un paso realmente importante para lograr tener código más flexible y mantenible es sin duda desarrollar software donde las subclases privadas no tienen comportamientos públicos diferentes en comparación con la clase base pública (o superclase).

Espero que hayan aprendido algo hoy y que puedan poner en práctica este principio en sus próximos desarrollos!

--

--

Genesis Sanguino
Swift en español

iOS Developer @MercadoLibre 🇨🇴 🇻🇪 & Clean Coder