El Principio de Sustitución de Liskov y su relación con la Programación por Contratos

El Principio de Sustitución de Likov fue acuñado por Barbara Liskov en el año 1987 durante una conferencia sobre Jerarquía y Abstraccióbn de datos. Su principal cometido es la de asegurar en la herencia entre clases de la Programación Orientada a Objetos que una clase derivada no únicamente es sino que debe comportarse como la clase base. Su definición es:

Si por cada objeto o1 del tipo S existe un objeto o2 del tipo T tal que para todos los programas P definidos en términos de T y el comportamiento de P permanece invariable cuando o1 es sustituido por o2, entonces S es un subtipo de T.

Pese a lo complejo que pueda llegar a parecer la definición en la practica lo podemos ver con algo más de claridad con el típico ejemplo del cuadrado y el rectángulo. En el siguiente snippet de código en C# se muestran las clases.

public abstract class Rectangulo
{
   public virtual int Ancho { get; set; }
   public virtual int Alto { get; set; }
   public abstract float Area();
}
public class Cuadrado : Rectangulo
{
    public override int Ancho  {
        get {
            return base.Ancho;
        }
        set {
            base.Ancho = value;
            base.Alto = value;
        }
    }
    public override int Alto  {
        get {
            return base.Alto;
        }
        set {
            base.Ancho = value;
            base.Alto = value;
        }
    }
    public override float Area()
    {
        return Ancho*Alto;
    }
}

Aparentemente a simple vista puede parecernos que la herencia es correcta, al menos desde el punto de vista técnico su implementación lo es, sin embargo el comportamiento es bien distinto. Mientras que en un Rectangulo el Ancho y el Alto pueden contener cualquier valor, un Cuadrado, por definición, el Ancho y el Alto debe ser del mismo valor, de otra forma seria no seria un cuadrado. Fijémonos en el setter de las propiedades Ancho y Alto de la clase Cuadrado; ambas se anulan entre sí. Podemos verlo más claro con un Test Unitario.


[TestFixture]
public class Test
{
    [Test]
    public void AreaRectangulo()
    {
        Rectangulo r = new Cuadrado
                            {
                                Ancho = 5, 
                                Alto = 2
                            };
        Assert.AreEqual(10.0f, r.Area());
    }
}

Basicamente comprobamos que, si un objecto Cuadrado con Ancho = 5 y Alto = 2, su Area, ya que asumimos que es un Rectangulo, es Ancho * Alto, es decir igual 10. El Test falla pues el setter de Alto impone además de éste el Ancho a 2, con lo cual el Area es 4.

Diseño por Contratos

Por otro lado, Bertrand Meyer acuñó el Diseño por Contratos (DbC) en el que se especifica un contrato a nivel de método para establecer los valores/tipos aceptables de entrada y de retorno, las condiciones de valor o de tipo para los errores y excepciones que puedan ocurrir, la precondición y postcondición y la invariantes.

Para cumplir con el Principio de Substitución de Liskov, la implementación de la clase derivada debe:

  • Ser menos restrictivas en la precondición.

  • Ser más restrictivas en la postcondición.

  • Preservar la invariancia.

En nuestro ejemplo podriamos decir que el contrato de las propiedad Ancho de Rectangulo seria:

[Precondicion(Ancho > 0)]
[Poscondicion("$after(Ancho)>0")]
[Poscondicion("$after(Alto)=$before(Alto)")]

Y el de Cuadrado:

[Precondicion(Ancho > 0)]
[Poscondicion("$after(Ancho)>0")]

En primer lugar, la propia naturaleza de la clase Cuadrado implica, precisamente para preservar la Invariancia, que la propiedad Ancho modifique, además, la propiedad Alto y esto deriva en que la postcondición de Cuadrado para el setter sea menos restrictiva que en Rectangulo y por tanto no cumpla con El Principio de Substitución de Liskov.

Conclusión

El Principio de Substitución de Liskov no dicta tanto el qué sino el cómo debe heredarse tipos. El uso de DbC está estrechamente ligado y puede ser de utilidad en herencias de tipos más complejos para preservar el comportamiento en la tipo derivado. El impacto de este principio en los nuevos lenguajes de desarrollo los podemos ver por ejemplo en el contravariancia de los argumentos de métodos para los subtipos o la covariancia para el retorno, tanto en delegados como en interfaces como es el caso de C#.

Imagen | Globalnerdy.com

Portada de Genbeta