O padrão do estado viola o princípio de substituição de Liskov?

17

Esta imagem é obtida em Aplicando padrões e design orientado a domínio: com exemplos em C # e .NET

insira a descrição da imagem aqui

Este é o diagrama de classes para o Padrão de Estado em que um SalesOrderpode ter estados diferentes durante seu tempo de vida. Apenas certas transições são permitidas entre os diferentes estados.

Agora a OrderStateclasse é uma abstractclasse e todos os seus métodos são herdados para suas subclasses. Se considerarmos a subclasse Cancelledque é um estado final que não permite nenhuma transição para outros estados, teremos que overridetodos os seus métodos nesta classe para lançar exceções.

Agora, isso não viola o princípio de substituição de Liskov, uma vez que uma sublcass não deve alterar o comportamento dos pais? Mudar a classe abstrata em uma interface corrige isso?
Como isso pode ser consertado?

Songo
fonte
Alterar OrderState para ser uma classe concreta que lança exceções "NotSupported" em todos os seus métodos por padrão?
James
Não li este livro, mas me parece estranho que OrderState contenha Cancel () e, em seguida, exista um estado Canceled, que é um subtipo diferente.
Euphoric
@Euphoric por que é estranho? chamar cancel()muda o estado para cancelado.
Songo 08/01
Quão? Como você pode chamar Cancelar no OrderState, quando a instância do estado no SalesOrder deve mudar. Eu acho que todo o modelo está errado. Eu gostaria de ver a implementação completa.
Euphoric

Respostas:

3

Esta implementação em particular, sim. Se você tornar os estados classes concretas em vez de implementadores abstratos, se afastará disso.

No entanto, o padrão de estado ao qual você está se referindo, que é efetivamente um design de máquina de estado, é em geral algo que discordo do modo como o vi crescer. Eu acho que há motivos suficientes para decretar isso como uma violação do princípio de responsabilidade única, porque esses padrões de gerenciamento de estado acabam sendo repositórios centrais para o conhecimento do estado atual de muitas outras partes de um sistema. Essa parte do gerenciamento de estado que está sendo centralizada geralmente exige que as regras de negócios relevantes para muitas partes diferentes do sistema as coordenem razoavelmente.

Imagine se todas as partes do sistema que se preocupam com o estado estivessem em serviços diferentes, processos diferentes em máquinas diferentes, um gerenciador de status central que detalha o status de cada um desses locais com gargalos efetivos em todo esse sistema distribuído e acho que gargalos é um sinal violação de SRP, bem como design geralmente ruim.

Por outro lado, eu sugeriria que os objetos fossem mais inteligentes, como no objeto Model no padrão MVC, onde o modelo sabe como lidar com ele mesmo, não precisa de um orquestrador externo para gerenciar o funcionamento interno ou o motivo.

Mesmo colocando um padrão de estado como esse dentro de um objeto para que ele esteja apenas se gerenciando, parece que você estaria tornando esse objeto muito grande. Os fluxos de trabalho devem ser feitos através da composição de vários objetos auto-responsáveis, eu diria, em vez de um único estado orquestrado que gerencia o fluxo de outros objetos ou o fluxo de inteligência dentro de si.

Mas, nesse ponto, é mais arte do que engenharia e, portanto, é definitivamente subjetiva sua abordagem a algumas dessas coisas, que disse que os princípios são um bom guia e sim a implementação que você listou é uma violação do LSP, mas pode ser corrigida para não ser. Apenas tenha muito cuidado com o SRP ao usar qualquer padrão dessa natureza e você provavelmente estará seguro.

Jimmy Hoffa
fonte
Basicamente, o grande problema é que a orientação a objetos é boa para adicionar novas classes, mas dificulta a adição de novos métodos. E no caso do estado, como você disse, não é provável que você precise estender o código com novos estados com muita frequência.
hugomg
3
@ Jimmy Hoffa Desculpe, mas o que você quer dizer exatamente com "Se você tornar os estados classes concretas em vez de implementadores abstratos, ficará longe disso". ?
Songo 08/01
@ Jimmy: Tentei implementar o estado sem o padrão de estado, fazendo com que cada componente soubesse apenas sobre seu próprio estado. Isso acabou fazendo com que grandes mudanças fluíssem significativamente mais complexas para implementar e manter. Dito isso, acho que usar uma máquina de estado (idealmente a biblioteca de outra pessoa para forçá-la a ser tratada como uma caixa preta) em vez do padrão de design de estado (portanto, estados são apenas elementos dentro de um enum, em vez de classes e transições completas, são apenas burros arestas) oferece muitos dos benefícios do padrão de estado, ao mesmo tempo em que é hostil às tentativas dos desenvolvedores de abusar dele.
31716 Brian
8

uma sublaca não deve alterar o comportamento dos pais?

Essa é uma má interpretação comum do LSP. Uma subclasse pode alterar o comportamento do pai, desde que permaneça fiel ao tipo pai.

Há uma boa e longa explicação na Wikipedia , sugerindo as coisas que violarão o LSP:

... há várias condições comportamentais que o subtipo deve atender. Eles são detalhados em uma terminologia semelhante à do design por metodologia de contrato, levando a algumas restrições sobre como os contratos podem interagir com a herança:

  • 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. Foi o novo elemento introduzido por Liskov e Wing. Uma violação dessa restrição pode ser exemplificada pela definição de um MutablePoint como um subtipo de um ImmutablePoint. Isso é uma violação da restrição do histórico, porque no histórico do ponto Imutável, o estado é sempre o mesmo após a criação, portanto, não pode incluir o histórico de um MutablePoint em geral. Os campos adicionados ao subtipo podem, no entanto, ser modificados com segurança porque não são observáveis ​​pelos métodos do supertipo.

Pessoalmente, acho mais fácil simplesmente lembrar disso: se eu estiver visualizando um parâmetro em um método que possui o tipo A, alguém passando um subtipo B me causará alguma surpresa? Se eles o fizerem, há uma violação do LSP.

Lançar uma exceção é uma surpresa? Na verdade não. É algo que pode acontecer a qualquer momento, esteja eu chamando o método Ship no OrderState ou Granted ou Shipped. Então eu tenho que dar conta disso e não é realmente uma violação do LSP.

Dito isto , acho que existem maneiras melhores de lidar com essa situação. Se eu estivesse escrevendo isso em C #, usaria interfaces e verificaria a implementação de uma interface antes de chamar o método. Por exemplo, se o OrderState atual não implementar IShippable, não chame um método Ship nele.

Mas também não usaria o padrão do Estado para essa situação específica. O padrão State é muito mais apropriado ao estado de um aplicativo do que ao estado de um objeto de domínio como este.

Portanto, em poucas palavras, este é um exemplo mal elaborado do Padrão de Estado e não é uma maneira particularmente boa de lidar com o estado de uma ordem. Mas, sem dúvida, não viola o LSP. E o padrão do Estado, por si só, certamente não.

pdr
fonte
Parece-me que você está confundindo LSP com Princípio de menor surpresa / espanto. Acredito que o LSP tenha limites mais claros onde isso os viola, embora você esteja aplicando a POLA mais subjetiva, baseada em expectativas subjetivas. ; isto é, cada pessoa espera algo diferente do que o próximo. O LSP é baseado em garantias contratuais e bem definidas dadas pelo fornecedor, não em expectativas subjetivamente avaliadas adivinhadas pelo consumidor.
Jimmy Hoffa
@JimmyHoffa: Você está certo, e foi por isso que expliquei o significado exato antes de declarar uma maneira mais simples de lembrar o LSP. No entanto, se houver uma dúvida em minha mente, o que significa "surpresa", então voltarei e verificarei as regras exatas do LSP (você pode realmente se lembrar delas à vontade?). As exceções que substituem a funcionalidade (ou vice-versa) são difíceis porque não violam nenhuma cláusula específica acima, o que provavelmente é uma pista de que ela deve ser tratada de uma maneira totalmente diferente.
Pd #
1
Uma exceção é uma condição esperada definida no contrato em minha mente; pense em um NetworkSocket; pode haver uma exceção esperada no envio se não estiver aberta; se sua implementação não lançar essa exceção para um soquete fechado, você está violando o LSP. , se o contrato indicar o contrário e seu subtipo lançar a exceção, você estará violando o LSP. É mais ou menos assim que eu classifico exceções no LSP. Quanto a lembrar as regras; Eu posso estar errado sobre isso e sinta-se à vontade para me dizer isso, mas meu entendimento de LSP vs POLA está definido na última frase do meu comentário acima.
Jimmy Hoffa
1
@ JimmyHoffa: Eu nunca havia considerado o POLA e o LSP conectados remotamente (penso no POLA em termos de interface do usuário), mas agora que você apontou, eles meio que são. Não tenho certeza se eles estão em conflito, ou que um é mais subjetivo que o outro. O LSP não é uma descrição inicial do POLA em termos de engenharia de software? Da mesma maneira que fico frustrado quando Ctrl-C não é Copy (POLA), também fico frustrado quando um ImmutablePoint é mutável porque na verdade é uma subclasse MutablePoint (LSP).
pdr
1
Eu consideraria uma CircleWithFixedCenterButMutableRadiusprovável violação do LSP se o contrato do consumidor ImmutablePointespecificar que X.equals(Y), se os consumidores puderem substituir X por Y livremente e vice-versa, e, portanto, devem abster-se de usar a classe de tal maneira que tal substituição possa causar problemas. O tipo pode ser capaz de definir legitimamente equalstal que todas as instâncias sejam comparadas como distintas, mas esse comportamento provavelmente limitaria sua utilidade.
Supercat 25/03
2

(Isso é escrito do ponto de vista do C #, portanto, não há exceções verificadas.)

Segundo o artigo da Wikipedia sobre LSP , uma das condições do LSP é:

Nenhuma nova exceção deve ser lançada pelos métodos do subtipo, exceto onde essas exceções são elas próprias subtipos de exceções lançadas pelos métodos do supertipo.

Como você deve entender isso? O que exatamente são “exceções geradas pelos métodos do supertipo” quando os métodos do supertipo são abstratos? Eu acho que essas são as exceções documentadas como exceções que podem ser lançadas pelos métodos do supertipo.

O que isso significa é que, se OrderState.Ship()estiver documentado como algo como "lança InvalidOperationExceptionse essas operações não forem suportadas pelo estado atual", acho que esse design não quebra o LSP. Por outro lado, se os métodos de supertipo não forem documentados dessa maneira, o LSP será violado.

Mas isso não significa que este seja um bom design, você não deve usar exceções para o fluxo de controle normal, o que parece muito próximo. Além disso, em um aplicativo real, você provavelmente gostaria de saber se uma operação pode ser executada antes de tentar fazê-lo, por exemplo, para desativar o botão "Enviar" na interface do usuário.

svick
fonte
Na verdade, esse é exatamente o ponto que me fez pensar. Se as subclasses lançarem exceções não definidas no pai, deve ser uma violação do LSP.
Songo 08/01
Eu acho que em um idioma onde as exceções não são verificadas, lançá-las não é uma violação do LSP. É apenas o conceito de uma linguagem em si, que em qualquer lugar a qualquer hora pode ser lançada qualquer exceção. Portanto, qualquer código de cliente deve estar pronto para isso.
Zapadlo