Principio de sustitución de Liskov (SOLID 3ª parte)
Antonio Leiva

Si ya leíste el principio Open/Closed, hoy vamos a hablar del principio de sustitución de Liskov:

Si quieres tenerlo más cómodo, puedes descargarte el contenido en formato PDF y leerlo donde quieras. Te he preparado esta guía de Principios SOLID para ti.

Principio de Sustitución de Liskov

El principio de sustitución de Liskov nos dice que si en alguna parte de nuestro código estamos usando una clase, y esta clase es extendida, tenemos que poder utilizar cualquiera de las clases hijas y que el programa siga siendo válido.

Esto nos obliga a asegurarnos de que cuando extendemos una clase no estamos alterando el comportamiento de la padre.

Este principio viene a desmentir la idea preconcebida de que las clases son una forma directa de modelar la realidad, y que hay que tener cuidado con esa modelización.

La primera en hablar de él fue Bárbara Liskov (de ahí el nombre), una reconocida ingeniera de software americana.

¿Cómo detectar que estamos violando el principio de sustitución de Liskov?

Seguro que te has encontrado con esta situación muchas veces: creas una clase que extiende de otra, pero de repente uno de los métodos te sobra, y no sabes que hacer con él.

Las opciones más rápidas son bien dejarlo vacío, bien lanzar una excepción cuando se use, asegurándote de que nadie llama incorrectamente a un método que no se puede utilizar.

Si un método sobrescrito no hace nada o lanza una excepción, es muy probable que estés violando el principio de sustitución de Liskov.

Si tu código estaba usando un método que para algunas concreciones ahora lanza una excepción, ¿cómo puedes estar seguro de que todo sigue funcionando?

Imagen de Derick Balley
Imagen de Derick Balley

Otra herramienta que te avisará fácilmente son los tests. Si los tests de la clase padre no funcionan para la hija, también estarás violando este principio.

Veremos un ejemplo con el primer caso.

Ejemplo

En la vida real tenemos claro que un elefante es un animal. Imaginemos que tenemos la clase Animal que representa un animal, y les damos a los animales la propiedad de andar y saltar:

open class Animal {
    open fun walk() { ... }
    open fun jump() { ... }
}

Y tenemos una parte del código donde recibimos un animal, y necesitamos que el animal salte:

fun jumpHole(a: Animal){
    a.walk()
    a.jump()
    a.walk()
}

Ahora nos creamos un elefante. Pero claro, un elefante no puede saltar, así que decidimos lanzar una excepción para asegurarnos de detectarlos si esto ocurre:

class Elephant : Animal() {
    override fun jump() =
        throw Exception("Los elefantes no pueden saltar")

}

Ahora en todos los sitios donde estemos usando jumpHole(), si el animal es un elefante, tendremos una excepción.

Mal asunto, ¿no?

¿Cómo lo solucionamos?

Aquí lo que tenemos que entender es que la abstracción que hemos decidido hacer no es correcta.

Hay animales que no saltan, así que estamos dando por ciertos casos que se pueden volver en nuestra contra.

Por tanto, las clases tienen que representar esos posibles estados inequívocamente, y las funciones usar las abstracciones que necesiten.

¿Qué podríamos hacer en este caso? Plantear un tipo de animal ligero que sí que puede saltar, mientras que damos por hecho que los animales en general no pueden hacerlo:

open class Animal {
    open fun walk() {  }
}

open class LightweightAnimal : Animal() {
    open fun jump() {  }
}

Esto nos permite definir animales que sí pueden saltar y otros que no. Por ejemplo un perro y un elefante:

class Dog: LightweightAnimal()

class Elephant: Animal()

Y la función de jumpHole() solo admitiría animales que pueden saltar:

fun jumpHole(a: LightweightAnimal){
    a.walk()
    a.jump()
    a.walk()
}

Elegir las abstracciones correctas muchas veces no es fácil, pero tenemos que intentar limitar al máximo cuál es su alcance para no pedir más de lo que se necesita ni menos.

Esta es la solución que obtendríamos mediante herencia aplicando el Principio de Liskov, pero también se podría haber solucionado mediante composición.

La herencia nos puede generar una jerarquía de clases muy compleja si hay muchos tipos de animales, así que en función del problema hay que plantearse cuál merece la pena usar.

Esta segunda opción es la que veremos con el Principio de segregación de interfaces

Si quieres tenerlo todo en un mismo sitio, puedes descargarte estos artículos como guía de Principios SOLID de forma gratuita.

Conclusión

El principio de Liskov nos ayuda a utilizar la herencia de forma correcta, y a tener mucho más cuidado a la hora de extender clases.

En la práctica nos ahorrará muchos errores derivados de nuestro afán por modelar lo que vemos en la vida real en clases siguiendo la misma lógica.

No siempre hay una modelización exacta, por lo que este principio nos ayudará a descubrir la mejor forma de hacerlo.

La cuarta parte tratará sobre el Principio de segregación de interfaces. ¿Qué te ha parecido hasta ahora? ¿Crees que tiene sentido aplicar estos principios en tu día a día?

Quizá también te interese…

Unidirectional Data Flow: Qué es y cómo funciona en Android

Unidirectional Data Flow: Qué es y cómo funciona en Android

Hay algunos conceptos que están empezando a resonar muy fuerte en Android, y Unidirectional Data Flow es uno de ellos. Como puede que sepas si estás apuntado a la newsletter, me he planteado escribir una serie de artículos sobre MVI (Model View Intent), y me he topado...

34 Comentarios

  1. Daniel Garay

    Buenas

    Me quedó un tanto abstracto este principio… algun otro ejemplo aplicado?

    Responder
  2. Antonio Leiva

    Imagina que tienes una clase `Animal`, que tiene un método `correr()`, y creas la clase `Tortuga` extendiendo de ella, sobrescribes el método lanzando una excepción “Las tortugas no pueden correr”. Esto viola el principio de sustitución de Liskov, porque si en tu código estabas usando un objeto `Animal animal = new Animal()`, y ahora en vez de eso haces `Animal animal = new Tortuga()`, el programa estará roto en cuanto se llame el método `correr()`.

    ¿Te queda más claro con este ejemplo?

    Responder
    • Andres David serrano

      con este ejemplo entendí mejor , gracias y buen trabajo a hora es empezar a aplicarlo

      Responder
      • elkinbernal21

        jajajja Andrés tiene razón, me fue mejor con este ejemplo sobre la tortuga

        Responder
    • Sebastian

      Antonio, primero, Muchas gracias por tomarte el trabajo de explicar estos conceptos digamoslo un poco abstractos… Estoy estudiandolos… en el ejemplo que das de la tortuga, bastaría tambien para no infringir este principio el quitar el metodo correr() de la clase abstracta padre, ya que no todos los animales corren? Más bien ponerlo en una interfaz?
      Estoy esperando a llegar al ID (Inversión de dependencias) es el que más me cuesta ya que aún no entiendo todas las funcionalidades de las interfaces aplicadas a patrones y buenas prácticas, en todos los sitios solo te las explican como una alternativa a la herencia multiple y como una alternativa solo para implementar metodos que se necesitan, pero no esa funcionalidad de mmm instanciarlas a través de clases que las implementen y así aplicar por ej patrones de diseño o principios como SOLID.

      Responder
      • Antonio Leiva

        Sí, exacto. No está bien dar por hecho que todos los animales corren.

        Responder
  3. Miguel J. Sesma

    Gracias Antonio, Esta ha sido una de las explicaciones de Liskov más simples y claras que he leído. Chapeau.

    Responder
    • Antonio Leiva

      Muchas gracias, la verdad es que son conceptos difíciles de poner en palabras, y lleva un rato organizarlo y saber como expresarlo. Me alegra saber que cumple su objetivo.

      Responder
  4. Manuel Calero Solís.

    Antonio, en el ultimo ejemplo de Square, el test tampoco funcionaria, por lo tanto no se cumpliría el principio de Liskov.

    Responder
    • Antonio Leiva

      El test anterior no valdría para el último código. Puesto que ya no existen setters, no tiene sentido un test que valide qué ocurre cuando se setean valores. Sin embargo, cualquier código que use Rectangle, ahora puede usar Square sin temor a que se modifique el comportamiento. En mi opinión sí que se cumple Liskov, pero igual se me está escapando algo.

      Responder
      • Sergio Pascual Seligrat

        Creo que para entender el concepto que es el objetivo del post está más que bien explicado.

        El test no se cumple por que se está comparando con un valor fijo dentro del propio test, y eso puede inducir a una mala interpretación del concepto.

        Responder
      • Jose

        Un objeto de la clase Square requiere un solo parámetro en el constructor y Rectangle requiere dos. Entonces, ¿como se cumple el principio de Liskov?

        Responder
      • Eduardo Liendo

        Estoy de acuerdo con lo que dice Manuel Calero Solís, pero tu respondes que “Sin embargo, cualquier código que use Rectangle, ahora puede usar Square sin temor a que se modifique el comportamiento” sin embargo esto no es cierto debido a que la hacer la instancia los constructores de ambos objetos son distintos ocasionando un error al querer intercambiar clases.

        Responder
  5. antoñin

    Habría que introducir una nueva clase entonces que fuera AnimalQueCorre que hereda de Animal. Tortuga heredaría de Animal pero no de AnimalQueCorre. Bien explicado.

    Me llama la atención lo de evitar métodos que no devuelvan nada. Con lo de lanzar la excepción queda más claro. El problema es que es algo muy común en el mantenimiento de grandes aplicaciones. Tienes 200 tablas, todo eran gacelas, tigres o guepardos… de repente un día hay un mantenimiento y te meten una tortuga.

    Dilema: ¿Cambio el código en 300 sitios en el gran monolito éste que es la app o Tortuga.Corre() lo convierto en un método vacío?

    Venga a ver quién es el guapo que se arriesga. Aquí es donde SOLID queda más en el ámbito académico. Tortuga corre! Y tortuga pasa de ti y sigue andando, lo aceptas y tira millas.

    Responder
    • Antonio Leiva

      El tema es que para poder refactorizar un código grande necesitas una batería de pruebas grande, que pruebe que tus cambios no van a romper todo lo demás. Yo no creo que SOLID sea algo académico, sólo que no es fácil encontrar el equilibrio entre volverse loco aplicándolo y no aplicarlo nada.

      Responder
    • uno que pasaba

      El tema de la orientación a objetos no tiene nada que ver con las “tablas”. La orientación a objetos y el modelo relacional son dos modelos diferentes y no puedes pasar de uno a otro.

      Y por otro lado, usar mejores abstracciones solucionarían el problema que comentas:
      En lugar de “AnimalQueCorre” que herede de “Animal” podriamos tener “Animal” con método “mover” y usar la composición en lugar de la herencia para este tema. Así la jerarquía de animales quedaría más limpia.

      public class Animal {
      private MotorLocomocion motorLocomocion;

      public Animal(MotorLocomocion motorLocomocion) {
      this.motorLocomocion = motorLocomocion;
      }
      public void mover() {
      motorLocomocion.mover();
      }

      }

      La interfaz de MotorLocomocion tendría varias implementaciones: volar, correr, nadar.

      No sé si me explico.

      Responder
      • JuanRamirezRuiz

        No veo clara esa solución. Podría haber algún animal que pudiese moverse de varias maneras: una ser humano, por ejemplo, puede correr y nadar (bueno, vale, depende de qué persona, pero, para el ejemplo, imagínate a un acuatleta).

        Responder
        • Raúl

          Siempre puedes implementar los métodos de locomoción a base de interfaces, o asignar distintos métodos mediante el patrón Decorator que se comentó en el principio open/closed no?

          Responder
  6. Locus

    El error esta en el ejemplo en sí: por contraintuitivo que parezca, un cuadrado no es un subtipo de un rectángulo. El criterio de especialización para el diseño de subclases no implica que cualquier especialización que se nos ocurra sea legítima. Este es un feo, y popular incluso en la universidad, ejemplo

    Responder
    • Antonio Leiva

      Yo sí creo que el ejemplo es bueno, primero porque en la vida real un cuadrado sí que es un rectángulo con sus lados iguales, y al aplicar orientación a objetos tendemos (a veces equivocadamente) a modelar directamente la realidad. Por tanto, este caso (y similares) se puede dar con bastante facilidad en el código de cualquier programa, y Liskov nos ayuda a detectar que igual no es buena idea esta modelización. Por otro lado, en el caso de que sean inmutables, sí que se cumple que un cuadrado sea una especialización de un rectángulo.

      Responder
    • Nelson Medinilla

      De acuerdo. La definición de subtipo está dada por el Principio de Sustitución. El problema parte de la idea equivocada de copiar la realidad en el software, donde parece que la herencia es el idea taxonómico del software. Y los apaños de inmutabilidad u otros son eso: apaños. El software es un universo con sus propiedades y el mundo exterior es distinto.

      Responder
    • Alejandro

      Considero que el error esta en el enfoque del ejemplo. En el test del rectángulo se utilizan las condiciones iniciales con valores de 4 y 5, mientras que al Cuadrado lo haces con valores de 4 por lado. Es claro que los resultados van a ser diferentes porque las condiciones iniciales son diferentes. Si probamos con un Rectángulo de lados 10 y 20 también va a dar un valor diferente, y no quiere decir que la clase tenga errores de abstracción. Quizás sea un ejemplo mas claro al comparar Rectágulos con Prismas (con una dimensión mas). Igualmente las explicaciones son muy claras. Te felicito y agradezco, logro despejarme algunas dudas.

      Responder
  7. Toto

    Al final lo más probable es que la clase tenga tan poco código QUE acabes teniendo un simple interfaz.

    Falta ese “que”, excelente artículo obviando esa minucia.

    Responder
    • Antonio Leiva

      Cambiado, muchas gracias!

      Responder
  8. Rodrigo

    Si he entendido bien la idea de este principio, es que si en la clase base (por ejemplo Animal) hay definido un método con una funcionalidad especifica en esa clase, si un subtipo de esa clase sobrescribe (override) ese método y cambia su funcionalidad (aplicando polimorfismo) a algo que se adapte más a su subtipo y cambiando la funcionalidad que hace el método de la clase base, al intentar cambiar el tipo base por el subtipo no funcionaria de la misma manera y se violaría este principio.

    Esto sería muy difícil de cumplir ya que si tenemos una clase base y los subtipos tienen los método de la clase base y además métodos suyos propios que necesitan para funcionar al intentar crear un objeto desde la clase base no tendría acceso a esos métodos teniendo que hacer un casting para saber de qué subtipo son, esto también estaría incumpliendo el principio ya que la clase base no puede acceder a esos métodos porque no los conoce y no se podría sustituir.

    ¿Como se haría el diseño para tener una clase base con algunos métodos que funcionen por igual en las clases subtipos y esas clases subtipos tengan sus propios métodos que no están en la clase base? Porque de esta manera tendrías que saber de que subtipo se trata y hacer un casting a ese tipo y esto incumpliría el principio open/close.

    Responder
  9. Nico M

    Muchas gracias por el ejemplo y la explicacion fueron muy utiles! Saludos querido!

    Responder
  10. Carlos

    Buena la explicación, utilice el ejercicio para conversar con mi equipo de trabajo, sin embargo te recomiendo ajustar un pequeño detalle:

    public interface IRectangle {
    int getWidth();
    int getHeight();
    int calculateArea();
    }

    public class Rectangle extends IRectangle {

    }

    public class Square extends IRectangle {

    }

    En el ejercicio extiendes de la interface, y como sabes, no se extiendes sino que se implementan.

    Saludos.

    Responder
  11. David

    Porque mencionas esto , acaso Dios no es el mejor de los programadores y nosotros somos los que no le entendemos ?
    “Este principio viene a desmentir la idea preconcebida de que las clases son una forma directa de modelar la realidad”. Esto no siempre es así, y el ejemplo más típico es el de un rectángulo y un cuadrado.
    En total desacuerdo.Quien observa la realidad aprende a programar mejor.

    Responder
    • Antonio Leiva

      Nadie dice que no, pero lo que es cierto es que la Programación Orientada a Objetos no encaja exactamente con nuestras ideas preconcebidas de la realidad. Así que no siemper hay que hacer modelos exactos, sino pensar más en cómo eso va a ayudar a que mis modelos puedan evolucionar mejor en el tiempo. Una cosa es la naturaleza y otra cómo nos comunicamos con la máquina, así que en términos más teológicos no me meto 😄

      Responder
  12. xavi

    Muchas gracias, muy clasificadoras tus explicaciones, me encanta el ejemplo de la tortuga!

    Responder
  13. Gabriel Mareño

    Con última construcción de clases se viola el principio de liskov. Humildemente creo que no solo es importarte la implementación de cada una de las clases, tanto de la padre como la hija porque, en este caso, no podría sustituir la clase padre por la clase hija ya que no tienen el mismo comportamiento y los resultados no son iguales. Si tienes :

    Rectangle r= new Rectangle(5,4);

    r.calculateArea();

    El resultado sería 20, pero si quiero comprobar el principio, entonces sustituiría de esta manera:

    Square r=new Square(5)

    r.calculateArea(); El resultado sería 25 y lo que quiero es poder pasarle dos parámetros para que se siga comportando como un rectángulo y no como un cuadrado. Se viola el principio, no puedo sustituir la clase padre por la hija

    Responder

Enviar un comentario

Los datos personales que proporciones a través de este formulario quedarán registrados en un fichero de Antonio Leiva Gordillo, con el fin de gestionar los comentarios que realizas en este blog. La legitimación se realiza a través del consentimiento de la parte interesada. Si no se acepta, no podrás comentar en este blog. Los datos que proporciona solo se utilizan para evitar el correo no deseado y no se usarán para nada más. Puede ejercer los derechos de acceso, rectificación, cancelación y oposición en contacto@devexperto.com.

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Acepto la política de privacidad *