Isto é uma violação do Princípio de Substituição de Liskov?

129

Digamos que temos uma lista de entidades de tarefas e um subtipo ProjectTask . As tarefas podem ser fechadas a qualquer momento, exceto ProjectTasks , que não pode ser fechado depois de ter um status Iniciado. A interface do usuário deve garantir que a opção de fechar um ProjectTask iniciado nunca esteja disponível, mas algumas salvaguardas estão presentes no domínio:

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();
     }
}

Agora, ao chamar Close() em uma tarefa, há uma chance de a chamada falhar se for um ProjectTask com o status iniciado, quando não seria, se fosse uma tarefa básica. Mas estes são os requisitos do negócio. Deve falhar. Isso pode ser considerado uma violação do princípio da substituição de Liskov ?

    
por Paul T Davies 16.10.2012 / 22:36
fonte

10 respostas

170

Sim, é uma violação do LSP. O princípio de substituição de Liskov requer que

  • As pré-condições não podem ser fortalecidas em um subtipo.
  • As pós-condições não podem ser enfraquecidas em um subtipo.
  • As invariantes do supertipo devem ser preservadas em um subtipo.
  • Restrição de histórico (a "regra do histórico"). Os objetos são considerados modificáveis somente através de seus métodos (encapsulamento). Como os subtipos podem introduzir métodos que não estão presentes no supertipo, a introdução desses métodos pode permitir mudanças de estado no subtipo que não são permissíveis no supertipo. A restrição de histórico proíbe isso.

Seu exemplo quebra o primeiro requisito reforçando uma condição prévia para chamar o método Close() .

Você pode consertar isso trazendo a pré-condição reforçada para o nível superior da hierarquia de herança:

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

Estipulando que uma chamada de Close() é válida somente no estado em que CanClose() retorna true , você faz com que a pré-condição se aplique ao Task , bem como ao ProjectTask , corrigindo o LSP violação:

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();
    }
}
    
por 16.10.2012 / 22:45
fonte
78

Sim. Isso viola o LSP.

Minha sugestão é adicionar CanClose método / propriedade para basear a tarefa, portanto, qualquer tarefa pode dizer se a tarefa nesse estado pode ser fechada. Também pode fornecer o motivo. E remova o virtual de Close .

Com base no meu comentário:

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);
    }
}
    
por 16.10.2012 / 22:44
fonte
24

O princípio de substituição de Liskov afirma que uma classe base deve ser substituível por qualquer de suas subclasses sem alterar nenhuma das propriedades desejáveis do programa. Uma vez que somente ProjectTask gera uma exceção quando fechado, um programa teria que ser alterado para acomodar, caso ProjectTask fosse usado em substituição a Task . Então é uma violação.

Mas se você modificar Task declarando em sua assinatura que pode gerar uma exceção quando fechada, então você não estaria violando o princípio.

    
por 16.10.2012 / 22:49
fonte
18

Uma violação de LSP requer três partes. O Tipo T, o Subtipo S e o programa P que usa T, mas recebe uma instância de S.

Sua pergunta forneceu T (Task) e S (ProjectTask), mas não P. Então sua pergunta está incompleta e a resposta está qualificada: Se existe um P que não espera uma exceção então, para esse P, você tem uma violação de LSP. Se todo P espera uma exceção, então não há violação de LSP.

No entanto, você faz tem uma violação SRP . O fato de que o estado de uma tarefa pode ser alterado, e a política de que certas tarefas em certos estados não devem ser alteradas para outros estados, são duas responsabilidades muito diferentes. / p>

  • Responsabilidade 1: representar uma tarefa.
  • Responsabilidade 2: implemente as políticas que alteram o estado das tarefas.

Estas duas responsabilidades mudam por diferentes razões e, portanto, devem estar em classes separadas. As tarefas devem lidar com o fato de ser uma tarefa e os dados associados a uma tarefa. TaskStatePolicy deve lidar com a transição de tarefas de estado para estado em um determinado aplicativo.

    
por 04.09.2013 / 18:00
fonte
16

Este pode ou não ser uma violação do LSP.

Sério. Me ouça.

Se você seguir o LSP, os objetos do tipo ProjectTask devem se comportar como se espera que os objetos do tipo Task se comportem.

O problema com o seu código é que você não documentou como os objetos do tipo Task devem se comportar. Você escreveu código, mas não tem contratos. Eu adicionarei um contrato para Task.Close . Dependendo do contrato que eu adiciono, o código para ProjectTask.Close segue ou não o LSP.

Dado o seguinte contrato para Task.Close, o código para ProjectTask.Close não segue o 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 o seguinte contrato para Task.Close, o código para ProjectTask.Close faz seguir o 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;
     }

Os métodos que podem ser substituídos devem ser documentados de duas maneiras:

  • Os documentos "Comportamento" podem ser usados por um cliente que sabe que o objeto do destinatário é Task , mas não sabe de que classe é uma instância direta. Ele também informa aos projetistas de subclasses quais substituições são razoáveis e quais não são razoáveis.

  • O "comportamento padrão" documenta o que pode ser invocado por um cliente que sabe que o objeto destinatário é uma instância direta de Task (ou seja, o que você obtém se usar new Task() . Ele também informa aos projetistas de subclasses qual comportamento será herdado se eles não substituírem o método.

Agora, as seguintes relações devem ser mantidas:

  • Se S é um subtipo de T, o comportamento documentado de S deve refinar o comportamento documentado de T.
  • Se S é um subtipo de (ou igual a) T, o comportamento do código de S deve refinar o comportamento documentado de T.
  • Se S é um subtipo de (ou igual a) T, o comportamento padrão de S deve refinar o comportamento documentado de T.
  • O comportamento real do código de uma classe deve refinar seu comportamento padrão documentado.
por 30.06.2015 / 16:28
fonte
6

Não é uma violação do Princípio de Substituição de Liskov.

O Princípio de Substituição de Liskov diz:

Let q(x) be a property provable about objects x of type T. Let S be a subtype of T. Type S violates the Liskov Substitution Principle if an object y of type S exists, such that q(y) is not provable.

O motivo, por que sua implementação do subtipo não é uma violação do Princípio de Substituição Liskov, é bastante simples: nada pode ser provado sobre o que o Task::Close() realmente faz. Claro, ProjectTask::Close() gera uma exceção quando Status == Status.Started , mas pode ser Status = Status.Closed em Task::Close() .

    
por 18.10.2012 / 21:17
fonte
4

Sim, é uma violação.

Eu sugiro que você tenha sua hierarquia de trás para frente. Se nem todos os Task puderem ser fechados, então close() não pertence a Task . Talvez você queira uma interface, CloseableTask , que todos os que não forem ProjectTasks possam implementar.

    
por 16.10.2012 / 22:42
fonte
3

Além de ser um problema do LSP, parece que ele está usando exceções para controlar o fluxo do programa (eu tenho que assumir que você captura essa exceção trivial em algum lugar e faz algum fluxo personalizado em vez de deixá-lo travar seu aplicativo).

Parece ser um bom local para implementar o padrão State para o TaskState e permitir que os objetos de estado gerenciem as transições válidas.

    
por 10.11.2012 / 04:29
fonte
1

Estou perdendo aqui uma coisa importante relacionada ao LSP e ao Design by Contract - em condições prévias, é o chamador cuja responsabilidade é garantir que as pré-condições sejam atendidas. O código chamado, na teoria do DbC, não deve verificar a pré-condição. O contrato deve especificar quando uma tarefa pode ser fechada (por exemplo, CanClose retorna True) e, em seguida, o código de chamada deve garantir que a condição prévia seja atendida antes de chamar Close ().

    
por 26.07.2018 / 19:01
fonte
0

Sim, é uma clara violação do LSP.

Algumas pessoas argumentam aqui que tornar explícito na classe base que as subclasses podem lançar exceções tornaria isso aceitável, mas não acho que isso seja verdade. Não importa o que você documenta na classe base ou em qual nível de abstração você move o código, as pré-condições ainda serão fortalecidas na subclasse, porque você adiciona a parte "Não é possível fechar uma tarefa de projeto iniciada" a ela. Isso não é algo que você possa resolver com uma solução alternativa, você precisa de um modelo diferente, que não viole o LSP (ou precisamos soltar a restrição "condições prévias não podem ser reforçadas").

Você pode tentar o padrão decorador se quiser evitar a violação de LSP neste caso. Pode funcionar, não sei.

    
por 10.10.2017 / 05:56
fonte