Digamos que temos uma lista de entidades de tarefas e um ProjectTask
subtipo. As tarefas podem ser fechadas a qualquer momento, exceto as ProjectTasks
que 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 ProjectTask
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()
uma tarefa, há uma chance de a chamada falhar se for ProjectTask
com 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 ?
design
object-oriented-design
solid
liskov-substitution
Paul T Davies
fonte
fonte
public Status Status { get; private set; }
:; caso contrário, oClose()
método pode ser contornado.Task
nã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.TaskCloser
processo queclosesAllTasks(tasks)
. Obviamente, esse processo não tenta capturar exceções; afinal, não faz parte do contrato explícito deTask.Close()
. Agora você apresentaProjectTask
e de repenteTaskCloser
começa a lançar exceções (possivelmente não tratadas). Este é um grande negócio!Respostas:
Sim, é uma violação do LSP. O princípio da substituição de Liskov exige que
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:
Ao estipular que uma chamada de
Close()
é válida apenas no estado quandoCanClose()
retorna,true
você aplica a pré-condição àTask
, assim como àProjectTask
, corrigindo a violação do LSP:fonte
Close
faça a verificação e adicionar um protegidoDoClose
seria 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.Sim. Isso viola o LSP.
Minha sugestão é adicionar
CanClose
mé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 deClose
.Com base no meu comentário:
fonte
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
ProjectTask
gera uma exceção quando fechado, um programa teria que ser alterado para se adaptar a isso, deveProjectTask
ser usado em substituição deTask
. Então é uma violação.Mas se você modificar
Task
afirmando em sua assinatura que ela pode gerar uma exceção quando fechada, não estaria violando o princípio.fonte
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.
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.
fonte
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.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
ProjectTask
deverão se comportar comoTask
se 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
Task
devem se comportar. Você escreveu código, mas não possui contratos. Vou adicionar um contrato paraTask.Close
. Dependendo do contrato que eu adiciono, o código paraProjectTask.Close
segue ou não o LSP.Dado o seguinte contrato para o Task.Close, o código para
ProjectTask.Close
não segue o LSP:Dado o seguinte contrato para Task.Close, o código para
ProjectTask.Close
não seguir o LSP: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 usarnew Task()
. Também informa aos designers das subclasses que comportamento será herdado se não o fizerem) substituir o métodoAgora, as seguintes relações devem ser mantidas:
fonte
Close
lanç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.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 ...Não é uma violação do princípio de substituição de Liskov.
O Princípio da Substituição de Liskov diz:
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 quandoStatus == Status.Started
, mas podeStatus = Status.Closed
entrarTask::Close()
.fonte
Sim, é uma violação.
Eu sugiro que você tenha sua hierarquia ao contrário. Se nem tudo
Task
é possível fechar, entãoclose()
não pertenceTask
. Talvez você queira uma interfaceCloseableTask
que todos os que nãoProjectTasks
podem implementar.fonte
Task
ele próprio não implementaCloseableTask
, eles estão fazendo uma conversão insegura em algum lugar para chamarClose()
.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.
fonte
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 ().
fonte
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.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.
fonte