Isso é uma violação do princípio da substituição de Liskov?


133

Digamos que temos uma lista de entidades de tarefas e um ProjectTasksubtipo. As tarefas podem ser fechadas a qualquer momento, exceto as ProjectTasksque não podem ser fechadas uma vez que tenham o status Iniciado. A interface do usuário deve garantir que a opção de fechar uma inicialização ProjectTasknunca 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()uma tarefa, há uma chance de a chamada falhar se for ProjectTaskcom o status iniciado, quando não seria se fosse uma tarefa base. Mas esses são os requisitos de negócios. Deveria falhar. Isso pode ser considerado uma violação do princípio da substituição de Liskov ?


14
Perfeito para um exemplo em T de violar a substituição liskov. Não use herança aqui, e você ficará bem.
Jimmy Hoffa

8
Você pode alterá-lo para public Status Status { get; private set; }:; caso contrário, o Close()método pode ser contornado.
Job

5
Talvez seja apenas este exemplo, mas não vejo nenhum benefício material em cumprir o LSP. Para mim, essa solução na pergunta é mais clara, mais fácil de entender e mais fácil de manter do que a que está em conformidade com o LSP.
Ben Lee

2
@BenLee Não é mais fácil de manter. Só parece assim porque você está vendo isso isoladamente. Quando o sistema é grande, certifique-se de que subtipos de Tasknão introduzam incompatibilidades bizarras no código polimórfico, que apenas conhece, Taské importante. O LSP não é um capricho, mas foi introduzido precisamente para ajudar na manutenção de grandes sistemas.
Andrés F.

8
@ BenLee Imagine que você tem um TaskCloserprocesso que closesAllTasks(tasks). Obviamente, esse processo não tenta capturar exceções; afinal, não faz parte do contrato explícito de Task.Close(). Agora você apresenta ProjectTaske de repente TaskClosercomeça a lançar exceções (possivelmente não tratadas). Este é um grande negócio!
Andres F.

Respostas:


174

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

  • As pré-condições não podem ser reforçadas em um subtipo.
  • As pós-condições não podem ser enfraquecidas em um subtipo.
  • Invariantes do supertipo devem ser preservados em um subtipo.
  • Restrição do histórico (a "regra do histórico"). Os objetos são considerados modificáveis ​​apenas por meio 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 alterações de estado no subtipo que não são permitidas no supertipo. A restrição de história proíbe isso.

Seu exemplo quebra o primeiro requisito, fortalecendo uma pré-condição para chamar o Close()método.

Você pode corrigi-lo, 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;
    }
}

Ao estipular que uma chamada de Close()é válida apenas no estado quando CanClose()retorna, truevocê aplica a pré-condição à Task, assim como à ProjectTask, corrigindo a violação do LSP:

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

17
Não gosto de duplicar esse cheque. Eu preferiria lançar exceção entrando em Task.Close e remover virtual de Fechar.
Euphoric

4
@Euphoric Isso é verdade, fazer com que o nível superior Closefaça a verificação e adicionar um protegido DoCloseseria uma alternativa válida. No entanto, eu queria ficar o mais próximo possível do exemplo do OP; melhorá-lo é uma questão separada.
dasblinkenlight

5
@Euphoric: Mas agora não há como responder à pergunta: "Esta tarefa pode ser encerrada?" sem tentar fechá-lo. Isso força desnecessariamente o uso de exceções para controle de fluxo. Admito, no entanto, que esse tipo de coisa pode ser levado longe demais. Levando longe demais, esse tipo de solução pode acabar gerando uma confusão empreendedora. Independentemente disso, a pergunta do OP me parece mais sobre princípios; portanto, uma resposta da torre de marfim é muito apropriada. 1
Brian

30
@ Brian O CanClose ainda está lá. Ainda pode ser chamado para verificar se a Tarefa pode ser fechada. A verificação em Fechar deve chamar isso também.
Euphoric

5
@Euphoric: Ah, eu não entendi. Você está certo, isso cria uma solução muito mais limpa.
Brian

82

Sim. Isso viola o LSP.

Minha sugestão é adicionar CanClosemétodo / propriedade à tarefa base, para que qualquer tarefa possa 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);
    }
}

3
Obrigado por isso, você levou o exemplo do dasblinkenlight mais adiante, mas eu gostei da explicação e da justificação. Desculpe, não posso aceitar 2 respostas!
Paul T Davies

Estou interessado em saber por que a assinatura é pública, pública, booleana, CanClose (fora do motivo da string) - ao esgotar você é apenas à prova de futuro? Ou há algo mais sutil que eu estou sentindo falta?
Reacher Gilt

3
@ReacherGilt Acho que você deve verificar o que / ref faz e ler meu código novamente. Você esta confuso. Simplesmente "Se a tarefa não puder ser fechada, quero saber o porquê".
Euphoric

2
out não está disponível em todos os idiomas, o retorno de uma tupla (ou um objeto simples que encapsule o motivo e o booleano o tornaria mais portátil entre os idiomas OO, embora com o custo de perder a facilidade de ter um bool diretamente. suporte para fora, nada de errado com esta resposta.
Newtopian

1
E é possível reforçar as condições prévias para a propriedade CanClose? Ou seja, adicionando a condição?
John V

24

O princípio de substituição de Liskov afirma que uma classe base deve ser substituível por qualquer uma de suas subclasses sem alterar nenhuma das propriedades desejáveis ​​do programa. Como somente ProjectTaskgera uma exceção quando fechado, um programa teria que ser alterado para se adaptar a isso, deve ProjectTaskser usado em substituição de Task. Então é uma violação.

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


Eu uso c # que eu acho que não tem essa possibilidade, mas eu sei que Java usa.
Paul T Davies

2
@PaulTDavies Você pode decorar um método com as exceções que ele lança, msdn.microsoft.com/en-us/library/5ast78ax.aspx . Você percebe isso quando passa o mouse sobre um método da biblioteca de classes base e obtém uma lista de exceções. Não é imposta, mas informa o chamador mesmo assim.
Despertar

18

Uma violação do LSP requer três partes. O tipo T, o subtipo S e o programa P que usa T, mas recebem uma instância de S.

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

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

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

Essas duas responsabilidades mudam por razões diferentes e, portanto, devem estar em classes separadas. As tarefas devem lidar com o fato de serem uma tarefa e os dados associados a uma tarefa. TaskStatePolicy deve lidar com a maneira como as tarefas passam de estado para estado em um determinado aplicativo.


2
As responsabilidades dependem muito do domínio e (neste exemplo) da complexidade dos estados das tarefas e de seus alteradores. Nesse caso, não há indicação disso, portanto não há problema com o SRP. Quanto à violação do LSP, acredito que todos assumimos que o chamador não espera uma exceção e o aplicativo deve mostrar uma mensagem razoável em vez de entrar em estado incorreto.
eufórico

Unca 'Bob responde? "Não somos dignos! Não somos dignos!". Enfim ... Se todo P espera uma exceção, não há violação do LSP. MAS, se estipularmos que uma instância T não pode gerar uma OpenTaskException(dica, dica) e todo P espera uma exceção , o que isso diz sobre o código para interface, não sobre a implementação? Do que estou falando? Eu não sei. Estou entusiasmado por comentar uma resposta de Unca 'Bob.
Radarbob 5/09/2013

3
Você está certo de que provar uma violação do LSP requer três objetos. No entanto, a violação LSP existe Se houver qualquer programa P que era correcta, na ausência de S, mas não com a adição de S.
Kevin Cline

16

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

A sério. Me ouça.

Se você seguir o LSP, os objetos do tipo ProjectTaskdeverão se comportar como Taskse espera que os objetos do tipo se comportem.

O problema com o seu código é que você não documentou como os objetos do tipo Taskdevem se comportar. Você escreveu código, mas não possui contratos. Vou adicionar um contrato para Task.Close. Dependendo do contrato que eu adiciono, o código para ProjectTask.Closesegue ou não o LSP.

Dado o seguinte contrato para o 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 não 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:

  • O "Comportamento" documenta o que pode ser invocado por um cliente que sabe que o objeto destinatário é um Task, mas não sabe de que classe é uma instância direta. Também informa aos projetistas das 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 Task(ou seja, o que você obtém se usar new Task(). Também informa aos designers das subclasses que comportamento será herdado se não o fizerem) substituir 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 para uma classe deve refinar seu comportamento padrão documentado.

@ user61852 levantou o ponto que você pode declarar na assinatura do método que pode gerar uma exceção e, simplesmente fazendo isso (algo que não tem nenhum código de efeito real) não está mais quebrando o LSP.
Paul T Davies

@PaulTDavies Você está certo. Mas na maioria dos idiomas, a assinatura não é uma boa maneira de declarar que uma rotina pode gerar uma exceção. Por exemplo, no OP (em C #, acho), a segunda implementação de Closelança. Portanto, a assinatura declara que uma exceção pode ser lançada - não diz que não. Java faz um trabalho melhor nesse sentido. Mesmo assim, se você declarar que um método pode declarar uma exceção, documente as circunstâncias sob as quais ele pode (ou irá). Portanto, argumento que, para ter certeza de que o LSP é violado, precisamos de documentação além da assinatura.
Theodore Norvell

4
Muitas respostas aqui parecem ignorar completamente o fato de que você não pode saber se um contrato é validado se não o conhecer. Obrigado por essa resposta.
gnasher729

Boa resposta, mas as outras respostas também são boas. Eles inferem que a classe base não gera exceção porque não há nada nessa classe que mostre sinais disso. Portanto, o programa, que usa a classe base, não deve se preparar para exceções.
Inf3rno 10/10

Você está certo de que a lista de exceções deve ser documentada em algum lugar. Eu acho que o melhor lugar é no código. Há uma pergunta relacionada aqui: stackoverflow.com/questions/16700130/… Mas você pode fazer isso sem anotações, etc ... também, basta escrever algo como if (false) throw new Exception("cannot start")a classe base. O compilador irá removê-lo, e ainda o código contém o que é necessário. Btw. ainda temos uma violação de LSP com estas soluções, porque a pré-condição ainda é reforçada ...
inf3rno

6

Não é uma violação do princípio de substituição de Liskov.

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

Vamos q (x) ser uma propriedade demonstrável sobre objetos x de tipo T . Deixe- S ser um subtipo de T . O tipo S viola o princípio de substituição de Liskov se um objeto y do tipo S existe, de forma que q (y) não é possível.

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


4

Sim, é uma violação.

Eu sugiro que você tenha sua hierarquia ao contrário. Se nem tudo Taské possível fechar, então close()não pertence Task. Talvez você queira uma interface CloseableTaskque todos os que não ProjectTaskspodem implementar.


3
Toda tarefa pode ser fechada, mas não sob todas as circunstâncias.
Paul T Davies

Essa abordagem parece arriscada para mim, pois as pessoas podem escrever código esperando que todas as tarefas implementem o ClosableTask, embora modelem com precisão o problema. Estou dividido entre essa abordagem e uma máquina de estado porque odeio máquinas de estado.
21712 Jimmy Jimmy -offoff

Se Taskele próprio não implementa CloseableTask, eles estão fazendo uma conversão insegura em algum lugar para chamar Close().
Tom G

@ TomG é disso que eu tenho medo #
Jimmy Hoffa

1
Já existe uma máquina de estado. O objeto não pode ser fechado porque está no estado errado.
Kaz

3

Além de ser um problema de LSP, parece que ele está usando exceções para controlar o fluxo do programa (devo presumir que você capturou essa exceção trivial em algum lugar e fez algum fluxo personalizado em vez de deixar o aplicativo travar).

Parece que este é um bom lugar para implementar o padrão State para TaskState e permitir que os objetos state gerenciem as transições válidas.


1

Falto aqui uma coisa importante relacionada ao LSP e ao Design by Contract - nas pré-condições, é o chamador cuja responsabilidade é garantir que as pré-condições sejam atendidas. O código chamado, na teoria 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 pré-condição seja atendida antes de chamar Close ().


O contrato deve especificar qualquer comportamento que a empresa precise. Nesse caso, esse Close () gerará uma exceção quando for iniciado ProjectTask. Esta é uma pós-condição (diz o que acontece depois que o método é chamado) e cumpri-lo é de responsabilidade do código chamado.
Goyo

@Goyo Sim, mas como outros disseram, a exceção é levantada no subtipo que fortaleceu a pré-condição e, portanto, violou o contrato (implícito) de que chamar Close () simplesmente fecha a tarefa.
Ezoela Vacca

Qual pré-condição? Eu não vejo nenhum.
Goyo

@Goyo Verifique a resposta aceita, por exemplo :) Na classe base, o Close não tem pré-condições, é chamado e fecha a tarefa. Na criança, no entanto, existe uma pré-condição sobre o status não ser iniciado. Como outros apontaram, esse é um critério mais forte e, portanto, o comportamento não é substituível.
Ezoela Vacca

Não importa, eu encontrei a pré-condição na pergunta. Porém, não há nada errado (em termos de DbC) com o código chamado, verificando pré-condições e gerando exceções quando elas não são atendidas. É chamado de "programação defensiva". Além disso, se houver uma pós-condição informando o que acontece quando a pré-condição não é atendida, como neste caso, a implementação deve verificar a pré-condição para garantir que a pós-condição seja atendida.
Goyo 26/07

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 para qual nível de abstração você move o código, as pré-condições ainda serão reforçadas na subclasse, porque você adiciona a parte "Não é possível fechar uma tarefa iniciada do projeto". Isso não é algo que você pode resolver com uma solução alternativa, você precisa de um modelo diferente, que não viole o LSP (ou precisamos relaxar na restrição "pré-condições não podem ser fortalecidas").

Você pode experimentar o padrão do decorador se quiser evitar a violação do LSP nesse caso. Pode funcionar, eu não sei.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.