Liskov Principio de sustitución: fortalecimiento de las condiciones previas

7

Estoy un poco confundido en cuanto a lo que realmente significa. En las preguntas relacionadas ( ¿Es esto una violación de ¿El principio de sustitución de Liskov? ), se dijo que el ejemplo viola claramente el LSP.

Pero me pregunto, si no se lanza una nueva excepción, ¿seguiría siendo una violación? ¿No es simplemente polimorfismo entonces? Es decir:

public class Task
{
     public Status Status { get; set; }

     public virtual void Close()
     {
         Status = Status.Closed;
     }
}

public class ProjectTask : Task
{
     public override void Close()
     {
          if (Status == Status.Started) 
          {
              base.Close(); 
          }
     }
}
    
pregunta John V 18.01.2018 - 10:38

6 respuestas

5

Depende.

Para validar el LSP, debe conocer el contrato preciso de la función Close . Si el código se ve así

public class Task
{
     // after a call to this method, the status must become "Closed"
     public virtual void Close()
     //...
}

entonces una clase derivada que ignora este comentario viola el LSP. Si, sin embargo, el código se ve así

public class Task
{
     // tries to close the the task 
     // (check Status afterwards to find out if it has worked)
     public virtual void Close()
     //...
}

entonces ProjectTask no viola el LSP.

Sin embargo, en caso de que no haya ningún comentario, un nombre de función como Close le da a IMHO una llamada bastante clara, la expectativa de establecer el estado en "Cerrado", y si la función no funciona de esa manera, ser al menos una violación del "Principio de menos asombro".

Tenga en cuenta también que algunos lenguajes de programación como Eiffel tienen soporte de lenguaje incorporado para contratos, por lo que uno no necesariamente necesita confiar en los comentarios. Consulte este artículo de Wikipedia para obtener una lista.

    
respondido por el Doc Brown 18.01.2018 - 11:58
20

No puede decidir si un código viola el LSP por el código mismo. Debe conocer el contrato que debe cumplir cada método.

En el ejemplo, no hay contrato explícito , por lo que debemos adivinar cuál será el contrato previsto del método Close() .

Mirando la implementación de la clase base del método Te Close (), el único efecto de ese método es que luego el Status es Status.Closed . Mi mejor conjetura de un contrato para este método dice:

  

Haz lo que sea necesario para que Status se convierta en Status.Closed .

Pero eso es solo una suposición plausible . Nadie puede estar seguro de eso si no está escrito explícitamente.

Demos por sentado mi suposición.

¿El método Close() anulado también cumple ese contrato ? Hay dos posibilidades de que después de ejecutar este método tengamos Status.Closed :

  • Ya teníamos Status.Closed antes de llamar al método.
  • Hemos tenido Status.Started . Luego llamamos a la implementación base, estableciendo el campo a Status.Closed .
  • En todos los demás casos, terminamos con un estado diferente.

Si Status solo tiene los dos valores posibles Closed y Started (por ejemplo, una enumeración de 2 valores), todo está bien, no hay violación de LSP, porque siempre obtenemos Status.Closed después del Close() método.

Pero probablemente hay más valores posibles de Status , que terminan en Status al no ser Status.Closed , por lo que violan el contrato .

El OP preguntó sobre la famosa frase "donde esté usando la clase base, se puede usar su clase derivada".

Así que me gustaría elaborar sobre eso.

Lo leí como "donde esté usando la clase base dentro de su contrato , su clase derivada se puede usar, sin violar ese contrato .

Por lo tanto, no solo se trata de no producir errores de compilación o de correr sin errores, se trata de hacer lo que exige el contrato .

Y solo se aplica a situaciones en las que le pido a la clase que haga algo que esté dentro de su rango de operaciones previsto. Por lo tanto, no debemos preocuparnos por las situaciones de abuso (por ejemplo, donde no se cumplen las condiciones previas).

Después de volver a leer su pregunta, creo que debería agregar un párrafo sobre el polimorfismo en ese contexto.

Polimorfismo significa que para instancias de diferentes clases, el mismo método de resultados resulta en diferentes implementaciones que se ejecutan. Por lo tanto, el polimorfismo técnicamente le permite anular nuestro método Close() con uno que, por ejemplo, abre una corriente. Técnicamente, eso es posible, pero es un mal uso del polimorfismo. Y un principio sobre los buenos y malos usos del polimorfismo es el LSP.

    
respondido por el Ralf Kleberhoff 18.01.2018 - 12:12
7

El Principio de Sustitución de Liskov se trata de contratos . Consiste en condiciones previas (condiciones que deben ser verdaderas para que pueda ejecutarse el comportamiento correspondiente), condiciones posteriores (condiciones que deben ser válidas para que se considere que el comportamiento termina su trabajo), invariantes (condiciones que deben cumplirse antes, durante y después de la operación). la ejecución del método correspondiente) y la restricción del historial (en mi opinión es un subconjunto de invariante, por lo que es mejor que compruebes wikipedia). En una pregunta que vinculó a un contrato implícito de la clase Task se parece a lo siguiente:

  • Precondición: no hay ninguna
  • condición posterior: Status es closed
  • Invariante: no puedo ver ninguno

Entonces, si una de las clases secundarias no cierra la tarea, se considera una violación de LSP dentro de algún contrato determinado .

Pero si postula explícitamente que su contrato es como "Cierre la tarea solo si es started ", entonces está bien. Puede hacerlo en su código; un ejemplo de esto es respuesta aceptada . Pero muy a menudo no puedes, por lo que puedes usar comentarios simples.

Básicamente, cada vez que piense en la violación de LSP, ya debe estar familiarizado con el contrato. No existe tal cosa como solo "violación de LSP", solo "violación de LSP dentro de algún contrato".

    
respondido por el Zapadlo 18.01.2018 - 11:34
1

Sí , sigue siendo una violación (probablemente)

Algunos clientes de Task confían en "después de Task::Close() , Status ahora es Closed ", y luego se rompe cuando encuentra un ProjectTask . Es posible que actualmente no tenga ninguno de estos clientes, pero entonces la condición posterior de Task::Close() tendría que ser " Status está en un estado válido pero no especificado", que básicamente no tiene sentido.

Lo mucho más natural es que Task::Close() tenga la condición posterior " Status es Closed ", lo que impide que la implementación en ProjectTask sea válida.

Este es un problema importante con los métodos void DoStuff() : todo lo que tienes son algunos efectos secundarios, por lo que tienes cosas que dependen de esos efectos secundarios. bool TryClose() tiene el significado " Close() si puedes, y cuéntamelo"

    
respondido por el Caleth 18.01.2018 - 10:47
1

Como lo mencionaron Ralf y otros, no ha implementado ni aplicado ningún contrato en su código, aparte de la supuesta convención "de sentido común" de que Close() debería dejar el objeto en un estado cerrado y distinto de los comentarios que se agregaron a la subclase.

En mi opinión, el ejemplo que ha proporcionado (sé que se copió de una publicación relacionada ) tiene un defecto de diseño para declarar el método Close() como virtual en la clase base Task - esto es solo invitar a otros Subclase Task y cambie el comportamiento, aunque haya proporcionado una implementación predeterminada que respeta el contrato.

Y lo que es peor, dado que Status no está encapsulado en absoluto, el estado es públicamente mutable, por lo que cualquier contrato en torno a Close carece de significado ya que el estado puede asignarse aleatoriamente de forma externa en cualquier caso.

Entonces, si la jerarquía de clases no requiere un comportamiento polimórfico de Close , simplemente eliminaría la palabra clave virtual en Task.Close :

// Encapsulate status, to control state transition
public Status Status { get; private set; }

public void Close()
{
    Status = Status.Closed;
}

(y haga lo mismo para cualquier otra transición de estado)

Si, sin embargo, SÍ requiere un comportamiento polimórfico (es decir, si las subclases necesitan proporcionar implementaciones personalizadas de Close ), entonces convertiría su clase base Task a una interfaz, y luego cumpliré las condiciones previas y posteriores mediante Contratos de código , de la siguiente manera:

[ContractClass(typeof(TaskContracts))]
public interface ITask
{
    Status Status { get; } // No externally accessible set

    void Close();
    // Other transition methods here.
}

Con los contratos correspondientes:

[ContractClassFor(typeof(ITask))]
public class TaskContracts : ITask
{
    public Status Status { get; }

    public void Close()
    {
        Contract.Requires(Status != Status.Closed, "Already Closed!");
        Contract.Ensures(Status == Status.Closed, "Must close Task on Completion!");
    }
}

El beneficio de este enfoque es que el contrato de uso de la interfaz es claro (y aplicable), y a diferencia del virtual Close() que se podría omitir, las subclases pueden proporcionar cualquier implementación que deseen, siempre que se cumpla el contrato.

    
respondido por el StuartLC 18.01.2018 - 12:45
0

Sí, todavía es una violación del LSP.

En la clase básica Tarea , después de que se haya invocado Cerrar () , el estado es Cerrado . En la clase ProjectTask derivada, después de invocar Close () , el estado puede o no ser Closed .

Por lo tanto, la condición posterior (el estado es Cerrado ) ya no se cumple en la clase ProjectTask .
O, en otras palabras, un cliente que solo sepa acerca de la Tarea puede confiar en el hecho de que el Estado está Cerrado después de invocar Cerrar () . Si le asigna una Tarea de Proyecto "disfrazada" como una Tarea (lo que se le permite hacer), y él invoca a Cerrar () , el resultado es diferente (el Estado podría no ser Cerrado ).

    
respondido por el CharonX 18.01.2018 - 11:27

Lea otras preguntas en las etiquetas