¿Cómo verificar el principio de sustitución de Liskov en una jerarquía de herencia?

14

Inspirado por esta respuesta:

El principio de sustitución de Liskov requiere que

  • Las condiciones previas no pueden fortalecerse en un subtipo.
  • Las condiciones posteriores no pueden debilitarse en un subtipo.
  • Las invariantes del supertipo deben conservarse en un subtipo.
  • Restricción del historial (la "regla del historial"). Los objetos se consideran modificables solo a través de sus métodos (encapsulación). Dado que los subtipos pueden introducir métodos que no están presentes en el supertipo, la introducción de estos métodos puede permitir cambios de estado en el subtipo que no están permitidos en el supertipo. La restricción de la historia lo prohíbe.

Esperaba que alguien publicara una jerarquía de clases que violara estos 4 puntos y cómo resolverlos en consecuencia.
Estoy buscando una explicación elaborada con fines educativos sobre cómo identificar cada uno de los 4 puntos en la jerarquía y la mejor manera de solucionarlo.

Nota:
esperaba publicar una muestra de código para que las personas trabajen, pero la pregunta en sí misma es sobre cómo identificar las jerarquías defectuosas :)

Songo
fuente
Hay otros ejemplos de violaciones de LSP en las respuestas a esta pregunta SO
StuartLC

Respuestas:

17

Es mucho más simple de lo que esa cita hace que suene, tan precisa como es.

Cuando observa una jerarquía de herencia, imagine un método que recibe un objeto de la clase base. Ahora pregúntese, ¿hay alguna suposición que alguien que edite este método pueda hacer que no sea válida para esa clase?

Por ejemplo ( visto originalmente en el sitio del tío Bob ):

public class Square : Rectangle
{
    public Square(double width) : base(width, width)
    {
    }

    public override double Width
    {
        set
        {
            base.Width = value;
            base.Height = value;
        }
        get
        {
            return base.Width;
        }
    }

    public override double Height
    {
        set
        {
            base.Width = value;
            base.Height = value;
        }
        get
        {
            return base.Height;
        }
    }
}

Parece bastante justo, ¿verdad? He creado un tipo de rectángulo especializado llamado Cuadrado, que sostiene que el Ancho debe ser igual a la Altura en todo momento. Un cuadrado es un rectángulo, por lo que encaja con los principios OO, ¿no?

Pero espera, ¿y si alguien ahora escribe este método?

public void Enlarge(Rectangle rect, double factor)
{
    rect.Width *= factor;
    rect.Height *= factor;
}

No es genial Pero no hay razón para que el autor de este método haya sabido que podría haber un problema potencial.

Cada vez que deriva una clase de otra, piense en la clase base y en lo que la gente podría asumir sobre ella (como "tiene un Ancho y una Altura y ambos serían independientes"). Entonces piense "¿esas suposiciones siguen siendo válidas en mi subclase?" Si no, reconsidere su diseño.

pdr
fuente
Muy buen y sutil ejemplo. +1. Lo que podría hacer es hacer que Ampliar sea un método de la clase Rectángulo y anularlo en la clase Cuadrado.
marco-fiset
@ marco-fiset: Prefiero ver Square y Rectangle desacoplados, Square con una sola dimensión, pero cada uno implementa IResizable. Es cierto que si hubiera un método Draw, serían similares, pero preferiría que ambos encapsularan una clase RectangleDrawer, que incluye el código común.
pdr
1
No creo que este sea un buen ejemplo. El problema es que un cuadrado no tiene ancho ni alto. Solo tiene una longitud de sus lados. El problema no estaría allí si el ancho y la altura solo fueran legibles, pero en este caso se pueden escribir. Cuando se introduce un estado modificable, siempre es mucho más difícil mantener el LSP.
SpaceTrucker
@pdr Gracias por el ejemplo, pero con respecto a las 4 condiciones que mencioné en mi publicación, ¿qué parte de la Squareclase las viola?
Songo
1
@Songo: es la restricción de la historia. Mejor explicado aquí: blackwasp.co.uk/LSP.aspx "Por su naturaleza, las subclases incluyen todos los métodos y propiedades de sus superclases. También pueden agregar miembros adicionales. La restricción del historial dice que los miembros nuevos o modificados no deben modificar el estado de un objeto de una manera que no estaría permitida por la clase base . Por ejemplo, si la clase base representa un objeto con un tamaño fijo, la subclase no debería permitir que se modifique este tamaño ".
pdr