Principio de sustitución de Liskov: si el subtipo tiene algún comportamiento adicional implementado, que no está presente en el tipo, ¿se trata de una violación del LSP?

8

En mi búsqueda por escribir un código mejor y más limpio, estoy aprendiendo acerca de los principios SOLID. En esto, LSP está demostrando ser poco difícil de entender adecuadamente.

Mi duda es ¿qué pasa si tengo algunos métodos adicionales en mi subtipo, S, que no estaban allí en tipo, T, esto siempre será una violación de LSP? En caso afirmativo, ¿cómo hago extendmis clases?

Por ejemplo, digamos que tenemos un Birdtipo. Y sus subtipos son Eagley Humming Bird. Ahora ambos subtipos tienen un comportamiento común como el Bird. Pero Eagletambién tiene un buen comportamiento depredador (que no está presente en el Birdtipo general ), que quiero usar . Por lo tanto, ahora no podré hacer esto:

Bird bird = new Eagle();

Entonces, ¿dar Eagleese comportamiento extra está rompiendo el LSP?

En caso afirmativo, ¿eso significa que no puedo extender mis clases porque eso causaría una violación de LSP?

class Eagle extends Bird {
   //we are extending Bird since Eagle has some extra behavior also
}

La extensión de clases debe permitirse de acuerdo con el principio de Abierto / Cerrado ¿verdad?

Gracias de antemano por responder! Como puede ver claramente, LSP me tiene confundido como cualquier cosa.

Editar: consulte esta respuesta SO. En esto nuevamente, cuando Cartiene un comportamiento adicional como ChangeGear, viola el LSP. Entonces, ¿cómo ampliamos una clase, sin violar el LSP?

usuario270386
fuente
Revisé eso, pero no respondió mi consulta. He leído muchas respuestas en realidad, pero hasta ahora no hay ayuda.
user270386
1
está justo en la respuesta superior, ¿lo ha leído: cada vez que deriva una clase de otra, piense en la clase base y en lo que la gente podría asumir sobre ella ... Luego piense "¿esas suposiciones siguen siendo válidas en mi subclase?" Si no, reconsidere su diseño.
mosquito
@gnat, sí, lo hice, pero soy un poco lenta :) Y generalmente necesito más explicaciones de las que otros podrían requerir. Después de leer la respuesta exhaustiva de David Arno, ahora puedo relacionarme con esa línea.
user270386
1
@FrankHileman muchos de nosotros trabajamos con compiladores y bases de código que no son ideales. Incluso si no lo hiciéramos, sigue siendo algo bueno cuando los humanos también entienden cómo respetarlos.
candied_orange

Respuestas:

10

Mi duda es ¿qué pasa si tengo algunos métodos adicionales en mi subtipo, S, que no estaban allí en tipo, T, esto siempre será una violación de LSP?

Respuesta muy simple: no.

El punto para el LSP es que Sdebe ser sustituible por T. Entonces, si Timplementa una deletefunción, también Sdebería implementarla y debería realizar una eliminación cuando se le llame. Sin embargo, Ses libre de agregar funcionalidades adicionales además de lo que Tofrece. Los consumidores de a T, cuando reciben una S, desconocen esta funcionalidad adicional, pero se permite que los consumidores la Sutilicen directamente.

Algunos ejemplos de cómo se puede violar el principio pueden ser:

class T
{
    bool delete(Item x)
    {
        if (item exists)
        {
            delete it
            return true;
        }
        return false;
    }
}

class S extends T
{
    bool delete(Item x)
    {
        if (item doesn't exist)
        {
            add it
            return false;
        }
        return true;
    }
}

Respuesta un poco más compleja: no, siempre y cuando no comience a afectar el estado u otro comportamiento esperado del tipo base.

Por ejemplo, lo siguiente sería una violación:

class Point2D
{
    private readonly double _x;
    private readonly double _y;

    public virtual double X => _x;
    public virtual double Y => _y;

    public Point2D(double x, double y) => (_x, _y) = (x, y);
}

class MyPoint2D : Point2D
{
    private double _x;
    private double _y;

    public override double X => _x;
    public override double Y => _y;

    public MyPoint2D(double x, double y) : 
        base(x, y) => (_x, _y) = (x, y);

    public void Update(double x, double y) => (_x, _y) = (x, y);
}

El tipo Point2D,, es inmutable; su estado no puede ser cambiado. Con MyPoint2D, he eludido deliberadamente ese comportamiento para hacerlo mutable. Eso rompe la restricción del historial Point2Dy, por lo tanto, es una violación del LSP.

David Arno
fuente
¿Quizás una buena adición a esta respuesta sería un ejemplo de comportamiento que podría agregarse a una deletefunción que sería una violación de LSP?
Andy Hunt
1
@ user270386: No. LSP no se trata de la estructura de la subclase, se trata del comportamiento del código de la subclase, en comparación con el comportamiento de la clase base. No es la funcionalidad adicional en sí misma la que viola la restricción del historial; más bien, la restricción se viola si esta nueva funcionalidad hace algo inesperado (por ejemplo, prohibido) en la clase base (y esto incluye cosas que se pueden expresar a través del lenguaje, así como cosas que solo se pueden expresar a través de la documentación).
Filip Milovanović
1
Vale la pena señalar que si T es una interfaz estricta, una clase completamente abstracta o incluso el patrón de objeto nulo, entonces S se trata más o menos de la estructura. T es el código. No es necesariamente una especificación, un documento de requisitos o un propietario del producto. Solo nos importan las violaciones de LSP cuando T se usa para manifestar una restricción que siempre debe cumplir. No todas las clases hacen eso. Pero si le dice a S que elimine y, por diseño, S no hace nada mejor que esté bien con el resto del código. No puedo decirte si eso es cierto. Solo puede decirle que es mejor que verifique antes de hacer esto.
candied_orange
1
(continuación) OMI, es el requisito de sustituibilidad lo que impulsa todo esto. La sustituibilidad realmente se deriva de la capacidad de tratar dos implementaciones diferentes (comportamientos) como equivalentes en algún nivel superior de abstracción prescrito por el contrato definido por el supertipo. La estructura es, en ese sentido, un medio para un fin.
Filip Milovanović
1
@DavidArno "el código del tipo base" - no es una especificación, ni necesariamente accesible.
Frank Hileman
3

Por supuesto no. Si el objeto Eagle puede ser utilizado por cualquier código que espera un Bird o una subclase, y se comporta como un Bird debería comportarse, está bien.

Por supuesto, el comportamiento de Eagle solo puede ser usado por código que es consciente de que es un objeto de este tipo. Es de esperar que algún código cree explícitamente un objeto Eagle y lo use como un objeto Eagle, al tiempo que puede usar cualquier código que espere objetos Bird.

gnasher729
fuente