David Lay

6 minute read

Los cinco principios de diseño de software SOLID fueron reunidos por Robert Martin (@unclebobmartin) a partir de varias ideas y papers pre-existentes en la comunidad. En estos artículos los revisaremos uno a uno con la interpretación desde mi experiencia con ellos y cómo han afectado mi forma de programar.

Todos los principios los puedes ver en el índice: http://www.davidlaym.com/2015/05/solid-resumen-de-los-principios

Principio de Substitución de Liskov

Principío de Substitución de Liskov

Principio de Substitución de Liskov

La L de SOLID es por “Liskov substitution principle”, que se puede traducir como “Principio de Substitución de Liskov”. Barbara Liskov describió este principio en 1988, de la siguiente manera:

“Necesitamos una propiedad de substitución tal que si por cada objeto O1 de tipo S hay un objeto O2 de tipo T de tal forma que para todos los programas P definidos en términos de T, el comportamiento de P permanece inalterado cuando O1 es substituido por O2, entonces S es un subtipo de T.”

Esto puede ser confuso para quienes no estamos acostumbrados a la redacción formal de la academia, pero en términos simples, Robert Martin lo resume como:

Los subtipos deben ser substituibles por sus tipos base.

Y queda implícito, pero me gustaría agregar:

Sin causar cambios en el comportamiento del programa que los utiliza.

Bajo este principio se engloban varios criterios importantes para tener en cuenta cuando se diseña una estructura de herencias:

  • No tener código dentro de una función que detecte el subtipo de una dependencia para ejecutar distintas funciones. (Gran no!)
  • No crear hijos que sobre-escriban comportamientos de la base
  • Asegurar que todos los clientes de la base pueden recibir todos los hijos existentes.
  • Tener mucho cuidado en la utilización de la heurística “es-un” para la detección de herencia, ya que lleva a  cometer violaciones a este principio.

Cuadrados y Rectángulos

Robert Martin ejemplifica con el caso del Rectángulo y el Cuadrado, el cual voy a intentar traducir y resumir un poco, ya que es bastante extenso (aunque muy claro).

Imaginemos que tenemos un programa que utiliza rectángulos y cuadrados. Dentro de la definición de diseño, se decidió que debido a que un cuadrado “es-un” rectángulo, entonces Cuadrado  deberá heredar de Rectangulo  (un cuadrado es un caso especial del rectángulo para ancho=alto).

public class Rectangulo {
    public virtual int Ancho { get; set; }
    public virtual int Alto  { get; set; }
    
    public virtual int CalculaArea() {
      return Ancho * Alto;
    }
}

public class Cuadrado : Rectangulo {
    private int _lado;
    public override int Alto {
        get {
            return _lado;
        }
        set {
            _lado = value;
        }
    }
    public override int Ancho {
        get {
            return _lado;
        }
        set {
            _lado = value;
        }
    }
}

En el rectángulo se puede establecer el alto y el ancho, por lo tanto, estos métodos son heredados al cuadrado, pero en el caso del cuadrado, no se pueden establecer estos valores de manera independiente ya que generaría inconsistencia. Los diseñadores deciden para este caso al establecer ya sea el alto o el ancho, ambas se actualizan. El cuadrado es consistente y el comportamiento es el que uno podría considerar razonable dada la abstracción.

La realidad llega y te golpea en la cara

Hay una consecuencia que solo se puede apreciar viendo el diseño cuando es utilizado por los distintos clientes. Imaginemos que tenemos una función que recibe un rectángulo, establece su ancho y su alto según el segundo y tercer parámetro, para luego verificar su área.

public function void ClienteRectangulo(Rectangulo rect, int ancho, int alto) {
    rect.Ancho = ancho;
    rect.Alto = alto;
    var area = rect.CalculaArea();
    if (area != ancho * alto) 
    {
        // niños, esto es solo un ejemplo, nunca emitan Exception 
        // diréctamente, usen una clase derivada apropiada.
        throw new Exception("error calculando el ancho");
   }
}

¿Qué pasa cuando a esta función se le entrega un Cuadrado en vez de un Rectangulo?

La respuesta es sencilla: Falla para todos los casos en que ancho != alto .

¿Cuál es el error?

No hay error en el código del cliente (es un poco forzado el ejemplo, pero es solo para mantenerlo simple), ya que no depende de detalles internos de la clase que utiliza, sino que utiliza la API pública expuesta y semánticamente la operación es esperable. Lo que sucede es que el Cuadrado  ha sobre-escrito el comportamiento de su base y los clientes que dependen de la base ahora no pueden utilizar a este hijo sin hacer casos especiales (lo que es un gran mal olor en el código).

Con una interfaz en vez de una herencia,  el problema se hace más patente. La interfaz tendría que contener ambas propiedades y el método (ancho, alto, área) pero el cuadrado no se conforma con este esquema y simplemente no sirve. En este caso un cuadrado NO ES un rectángulo y punto.

El motivo detrás del principio

Si no está claro aun, quiero ser muy explícito en el motivo de que se quiere cumplir con este principio: consistencia funcional. No es aceptable que haya una posibilidad de que tu código sea utilizado de manera sintácticamente correcta (el compilador no reclama) y al mismo tiempo aparentemente correcta (los nombres hacen sentido, hay lógica en el diseño) pero falla o tiene comportamiento irregular. Los errores de este tipo son muy difíciles de detectar y cuestan horas y horas de debug.

Corregir el diseño

Para corregir esta violación del LSP, se puede crear una clase abstracta que contenga solo el método CalcularArea()  (sin las propiedades de ancho y alto) definido como abstracto (obligar a los hijos a implementarlo). De esta forma, la clase abstracta permite reutilizar código, definir un contrato y al mismo tiempo, permitir que los clientes tomen una decisión más informada sobre de que tipo dependerán. Cuadrado  y  Rectangulo heredarán de esta clase abstracta.

Si no hay una cantidad de código que reutilizar, se puede evitar todo el acoplamiento de una clase abstracta y usar una interfaz que implementen los hijos con el método CalcularArea().

En cualquiera de los casos los clientes que necesiten calcular el área podrán depender de la abstracción y aquellos que manipulen dimensiones pedirán un Rectangulo  o un Cuadrado  directamente.

Conclusión

En su código probablemente van a encontrar violaciones al LSP en donde probablemente haya que considerar muchos clientes diferentes y estructuras de jerarquía más complejas. La fórmula siempre es la misma: re-diseñar la jerarquía teniendo en cuenta los clientes que utilizan las clases y no dejarse llevar por la semántica de las relaciones, siempre pensando en el caso “¿Que pasa si a este método le entrego este otro hijo?”.

 

 

comments powered by Disqus