¿Es esta una violación del Principio de Sustitución de Liskov?

128

Supongamos que tenemos una lista de entidades de tarea y un subtipo ProjectTask . Las tareas se pueden cerrar en cualquier momento, excepto ProjectTasks , que no se puede cerrar una vez que tienen un estado de Iniciado. La interfaz de usuario debe garantizar que la opción de cerrar un ProjectTask iniciado nunca esté disponible, pero hay algunas medidas de seguridad en el dominio:

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) 
              throw new Exception("Cannot close a started Project Task");

          base.Close();
     }
}

Ahora, cuando se llama a Close() en una Tarea, existe la posibilidad de que la llamada falle si es un ProjectTask con el estado iniciado, cuando no lo haría si fuera una Tarea base. Pero estos son los requisitos del negocio. Debería fallar. ¿Se puede considerar esto como una violación del principio de sustitución de Liskov ?

    
pregunta Paul T Davies 16.10.2012 - 22:36

10 respuestas

170

Sí, es una violación del LSP. El principio de sustitución de Liskov requiere que

  • Las condiciones previas no se pueden reforzar en un subtipo.
  • Las condiciones posteriores no se pueden debilitar en un subtipo.
  • Las invariantes del supertipo se deben conservar en un subtipo.
  • Restricción de historial (la "regla de historial"). Se considera que los objetos son 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.

Su ejemplo rompe el primer requisito al fortalecer una condición previa para llamar al método Close() .

Puede solucionarlo llevando la condición previa reforzada al nivel superior de la jerarquía de herencia:

public class Task {
    public Status Status { get; set; }
    public virtual bool CanClose() {
        return true;
    }
    public virtual void Close() {
        Status = Status.Closed;
    }
}

Al estipular que una llamada de Close() es válida solo en el estado cuando CanClose() devuelve true , la condición previa se aplica tanto al Task como al ProjectTask , que corrige el LSP. violación:

public class ProjectTask : Task {
    public override bool CanClose() {
        return Status != Status.Started;
    }
    public override void Close() {
        if (Status == Status.Started) 
            throw new Exception("Cannot close a started Project Task");
        base.Close();
    }
}
    
respondido por el dasblinkenlight 16.10.2012 - 22:45
78

Sí. Esto viola el LSP.

Mi sugerencia es agregar CanClose method / property a la tarea base, para que cualquier tarea pueda saber si la tarea en este estado se puede cerrar. También puede proporcionar la razón por qué. Y elimina lo virtual de Close .

Basado en mi comentario:

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

    public virtual bool CanClose(out String reason) {
        reason = null;
        return true;
    }
    public void Close() {
        String reason;
        if (!CanClose(out reason))
            throw new Exception(reason);

        Status = Status.Closed;
    }
}

public ProjectTask : Task {
    public override bool CanClose(out String reason) {
        if (Status != Status.Started)
        {
            reason = "Cannot close a started Project Task";
            return false;
        }
        return base.CanClose(out reason);
    }
}
    
respondido por el Euphoric 16.10.2012 - 22:44
24

El principio de sustitución de Liskov establece que una clase base debe ser reemplazable con cualquiera de sus subclases sin alterar ninguna de las propiedades deseables del programa. Dado que solo ProjectTask genera una excepción cuando se cierra, un programa tendría que cambiarse para adaptarse a eso, si se utilizara ProjectTask en sustitución de Task . Así que es una violación.

Pero si modifica Task indicando en su firma que puede generar una excepción cuando se cierre, entonces no estaría violando el principio.

    
respondido por el Tulains Córdova 16.10.2012 - 22:49
18

Una violación de LSP requiere tres partes. El Tipo T, el Subtipo S y el programa P que usa T pero se le da una instancia de S

Su pregunta ha proporcionado T (Tarea) y S (ProjectTask), pero no P. Entonces, su pregunta está incompleta y la respuesta está calificada: si existe una P que no espera una excepción, entonces, para esa P, usted tener una violación de LSP. Si cada P espera una excepción, entonces no hay violación de LSP.

Sin embargo, do tiene una infracción de SRP . El hecho de que el estado de una tarea se puede cambiar y la política de que ciertas tareas en ciertos estados no deberían cambiarse a otros estados, son dos responsabilidades muy diferentes.

  • Responsabilidad 1: Representar una tarea.
  • Responsabilidad 2: implementar las políticas que cambian el estado de las tareas.

Estas dos responsabilidades cambian por diferentes razones y, por lo tanto, deben estar en clases separadas. Las tareas deben manejar el hecho de ser una tarea y los datos asociados con una tarea. TaskStatePolicy debe manejar la forma en que las tareas pasan de un estado a otro en una aplicación determinada.

    
respondido por el Robert Martin 04.09.2013 - 18:00
16

Este puede o no ser una violación del LSP.

En serio. Escúchame.

Si sigue el LSP, los objetos del tipo ProjectTask deben comportarse como se espera que se comporten los objetos del tipo Task .

El problema con su código es que no ha documentado cómo se espera que se comporten los objetos de tipo Task . Has escrito código, pero no hay contratos. Agregaré un contrato para Task.Close . Dependiendo del contrato que agregué, el código para ProjectTask.Close cumple o no sigue el LSP.

Dado el siguiente contrato para Task.Close, el código para ProjectTask.Close no sigue el LSP:

     // Behaviour: Moves the task to the closed state
     // and does not throw any Exception.
     // Default behaviour: Moves the task to the closed state
     // and does not throw any Exception.
     public virtual void Close()
     {
         Status = Status.Closed;
     }

Dado el siguiente contrato para Task.Close, el código para ProjectTask.Close sigue el LSP:

     // Behaviour: Moves the task to the closed status if possible.
     // If this is not possible, this method throws an Exception
     // and leaves the status unchanged.
     // Default behaviour: Moves the task to the closed state
     // and does not throw any Exception.
     public virtual void Close()
     {
         Status = Status.Closed;
     }

Los métodos que pueden anularse deben documentarse de dos maneras:

  • El "Comportamiento" documenta en qué puede confiar el cliente que sabe que el objeto del destinatario es un Task , pero no sabe de qué clase es una instancia directa. También les dice a los diseñadores de las subclases cuáles sustituciones son razonables y cuáles no son razonables.

  • El "comportamiento predeterminado" documenta en qué puede confiar un cliente que sabe que el objeto del destinatario es una instancia directa de Task (es decir, qué obtiene si usa new Task() . También le dice a los diseñadores de las subclases, qué comportamiento se heredará si no anulan el método.

Ahora deben mantenerse las siguientes relaciones:

  • Si S es un subtipo de T, el comportamiento documentado de S debería refinar el comportamiento documentado de T.
  • Si S es un subtipo de (o igual a) T, el comportamiento del código de S debe refinar el comportamiento documentado de T.
  • Si S es un subtipo de (o igual a) T, el comportamiento predeterminado de S debería refinar el comportamiento documentado de T.
  • El comportamiento real del código para una clase debe refinar su comportamiento predeterminado documentado.
respondido por el Theodore Norvell 30.06.2015 - 16:28
6

No es una violación del principio de sustitución de Liskov.

El principio de sustitución de Liskov dice:

  

Deje que q (x) sea una propiedad comprensible sobre los objetos x de tipo T . Sea S un subtipo de T . El tipo S viola el principio de sustitución de Liskov si existe un objeto y de tipo S , de manera que q (y) no es demostrable.

La razón por la que su implementación del subtipo no es una violación del Principio de Sustitución de Liskov, es bastante simple: no se puede probar nada sobre lo que Task::Close() realmente hace. Claro, ProjectTask::Close() lanza una excepción cuando Status == Status.Started , pero también podría Status = Status.Closed en Task::Close() .

    
respondido por el Oswald 18.10.2012 - 21:17
4

Sí, es una violación.

Le sugiero que tenga su jerarquía hacia atrás. Si no se puede cerrar cada Task , entonces close() no pertenece a Task . Quizás desee una interfaz, CloseableTask que todos los que no sean ProjectTasks puedan implementar.

    
respondido por el Tom G 16.10.2012 - 22:42
3

Además de ser un problema de LSP, parece que está utilizando excepciones para controlar el flujo del programa (tengo que asumir que atrapas esta excepción trivial en algún lugar y haces un flujo personalizado en lugar de dejar que se bloquee tu aplicación).

Parece que este es un buen lugar para implementar el patrón de estado de TaskState y permitir que los objetos de estado administren las transiciones válidas.

    
respondido por el Ed Hastings 10.11.2012 - 04:29
1

Aquí falto algo importante relacionado con el LSP y el Diseño por Contrato: en las condiciones previas, es la persona que llama cuya responsabilidad es asegurarse de que se cumplan las condiciones previas. El código llamado, en la teoría DbC, no debe verificar la condición previa. El contrato debe especificar cuándo se puede cerrar una tarea (por ejemplo, CanClose devuelve True) y luego el código de llamada debe garantizar que se cumpla la condición previa, antes de que se llame a Cerrar ().

    
respondido por el Ezoela Vacca 26.07.2018 - 19:01
0

Sí, es una clara violación de LSP.

Algunas personas argumentan aquí que hacer explícito en la clase base que las subclases pueden generar excepciones haría esto aceptable, pero no creo que eso sea cierto. Independientemente de lo que documente en la clase base o al nivel de abstracción al que mueva el código, las precondiciones aún se reforzarán en la subclase, porque le agrega la parte "No se puede cerrar una tarea de proyecto iniciada". Esto no es algo que pueda resolverse con una solución alternativa, necesita un modelo diferente, que no infrinja el LSP (o debemos aflojarnos en la restricción de "las condiciones previas no se pueden reforzar").

Puede probar el patrón de decorador si desea evitar la violación de LSP en este caso. Podría funcionar, no lo sé.

    
respondido por el inf3rno 10.10.2017 - 05:56

Lea otras preguntas en las etiquetas