Liskov Substitution Principle (LSP)

Liskov Substitution Principle (LSP)

¿Qué es el Liskov Substitution Principle? ¿Cómo se aplica en React?

En pocas palabras lo que nos viene a decir el Principio de Substitución de Liskov (Liskov Substitution Principle, LSP) (la L dentro de los principio SOLID) es que un objeto que herede de una clase padre (un subtipo de objecto desde el punto de vista del padre en la jerarquía de herencia) debería poder ser sustituible por un objeto padre en dicha jerarquía de herencia.

¿Cónfunso, verdad? La respuesta es que sí, que así explicado como que cuesta verlo pero si lo explicamos con ejemplo es más que probable que las cosas queden mucho más claras. Pensemos por ejemplo en una clase que representa a un Animal, una clase hija de la misma podría ser la clase Perro. Bien, pues el LSP lo que nos viene a decir es que cualquier parte de nuestro código donde estuviésemos usando la clase Perro deberíamos poder usar la clase Animal sin que nuestro código se rompiese.

Esto que hemos explicado así de palabra lo podemos ver en el siguinte código donde tenemos una jerarquía de herencia en la que se presenta a la clase padre Animal en la que se define el método swin y dos clases hijas Dog y Cat que tienen su propia implementación de este método.

export class Animal {
  swim(distance: number) {
    console.log(`${distance} meters`)
  }
}

export class Dog extends Animal {
  swim(distance: number) {
    console.log(`${distance} meters`)
  }
}

export class Cat extends Animal {
  swin(distance: number) {
    new Error(`Cannot swin`)
  }
}

Atendiendo a la definición del LSP veremos que que aquí se está rompiendo porque en principio la clase Dog y la clase Cat deberían poder ser intercambiables pero no es así, puesto que si llamamos al método swin dentro de un objeto Cat se produciría un error y por lo tanto nuestra aplicación se rompería.

const cat = new Cat()
cat.swin() // Se lanza un error.

Nota: este mismo ejemplo es replicable en el caso de una jerarquí de aves puesto que podríamos tener la clase padre a todas las aves Ave en la que se defina el método volar() de tal manera que la mayoría de los pájaros que estén representando en las clases hijas podrían volar pero hay determinados tipos de aves (como lo pingüinos) que no vuelan y por lo tanto lanzarían un error cuando se llamase a su implementación del método volar().

¿Cómo podemos solucionar este problema y lograr que se cumpla el LSP? Pues la clave está en la propia jerarquía de herencia que hemos creado. Así deberíamos pensar en que en vez de tener una super clase genérica que englobe a todos los animales (la clase Animal en el ejemplo anterior) podríamos a su vez tener una serie de subclases intermedias que nos ayuden a clasificarlos.

Esto quiere decir que si en nuestra jerarquía anterior ahora hacemos el siguiente refactor donde introducimos la clase intermedia SwinningAnimal de la que deberían heredar todos los animales que efectivamente van a saber nadar dejándonos algo como lo siguiente:

export class Animal {}

export class SwimmingAnimal {
  swim(distance: number) {
    console.log(`${distance} meters`)
  }
}

export class Dog extends SwimmingAnimal {
  swim(distance: number) {
    console.log(`${distance} meters`)
  }
}

export class Cat extends Animal {}

LSP en React

El ejemplo que hemos usado es muy general y proviene de la programación orientada a objetos pero ¿cómo se puede ver el LSP en React? Pues vamos a partir de la definición de un componente muy simple que utilizaríamos para definir un botón en una aplicación:

const Button = ({ children, color, size }) => (
  <button
    style={{
      color,
      fontSize: size === 'xl' ? '32px' : '16px
    }}
  >
    {children}
  </button>
)

Ahora supongamos que tenemos un componente que vamos a usar para poder renderizar un botón de color rojo y cuya definición es la siguiente:

const RedButton = ({ children, isBig }) => (
  <Button color="red" size={isBig ? 'xl' : 'sm'}>
    ){children}
  </Button>
)

Como podemos ver en la implementación de RedButton hace uso del componente Button para lograr renderizar el color de botón rojo tal y como esperamos que suceda.

Ahora bien ¿cuál será uno de estos problemas en esta implementación y que estaría derivado de no estar siguiendo el LSP? Pues para entenderlo tenemos que pensar en la existencia de un tercer componente en el que estaríamos usando el componente RedButton como sigue:

return <RedButton isBig={true}>Click me</RedButton>

Ahora imaginemos que a medida que va a evolucionando nuestra aplicación nos encontramos en la situación en la que queremos cambiar el botón rojo (el componente RedButton) por el componente que a su vez utiliza este componente para componerse (es decir, que queremos cambiar el componente RedButton por el componente Button). Pues si se estuviese siguiendo el LSP simplemente deberíamos escribir lo siguiente:

return <Button isBig={true}>Click me</Button>

Y esto provocará un error puesto que el componente Button no tiene la prop isBig y por lo tanto se estará rompiendo el LSP porque aunque el componente RedButton esté internamente utilizando el componente Button en la implementación y utilización que se hace dentro del mismo le hemos cambiado el comportamiento de las props que van hacia abajo, ya que estamos usando la prop isBig para determinar el valor que le asignaremos a la prop size del componente Button.

En este caso RedButton no está herendado de Button como se podría pensar en el caso de estar hablando de la programación orientada a objetos pero sí que podríamos decir que pero sí que podemos decir que desde el punto de vista del LSP un RedButton que es un subtipo (esto entre muchas comillas) de otro tipo (el componente Button) porque dentro de su código se estará haciendo uso del mismo (tenemos que tener en cuenta que Button es el componente padre a partir del cual se está generando el RedButton).

¿Cómo lo solucionaríamos? Pues simplemente deberíamos eliminar la prop isBig del componente RedButton y definir la prop size garantizando además que los valores que pueda adoptar sean totalmente compatibles con los valores que espera recibir el componente Button en su prop size. Es decir, definiríamos:

const RedButton = ({ children, size }) => (
  <Button color="red" size={size}>
    ){children}
  </Button>
)

Y ahora podríamos hacer lo siguiente en el código del componente donde lo utilizaríamos:

return <RedButton size="xl">Click me</RedButton>

Esto nos va a permitir que el día de mañana, si fuese preciso, podríamos usar tanto el tipo RedButton como el supertipo Button en el mismo componente sin que en ningún momento se rompa nuestro código:

return <Button size="xl">Click me</Button>

Pero si nos fijamos en un detalle más aunque no existiría un cambio en el comportamiento de cómo se comportaría el botón sí que habría un cambio en la interfaz puesto que al haber cambiado RedButton por Button deberíamos añadir el color en que queremos que se renderice nuestro botón:

return (
  <Button color="red" size="xl">
    Click me
  </Button>
)

Pero con esto estaremos evitando que al haber usado el subtipo RedButton se esté rompiendo lo que podríamos hacer con el tipo Button. Dicho de otra manera, no existe un cambio en el comportamiento a la hora de usar un componente padre y un componente hijo tal y como pide el LSP.

Resumen: En definitiva el LSP nos viene a decir que cualquier subtipo debería poder substituido por su tipo padre sin ningún problema lo que implica que el RedButton debería poder ser cambiado fácilmente por el Button sin que se rompa absolutamente nada.