Estou tentando entender os princípios do SOLID do OOP e cheguei à conclusão de que o LSP e o OCP têm algumas semelhanças (se não para dizer mais).
o princípio aberto / fechado declara "entidades de software (classes, módulos, funções, etc.) devem estar abertas para extensão, mas fechadas para modificação".
O LSP, em palavras simples, afirma que qualquer instância de Foo
pode ser substituída por qualquer instância da Bar
qual é derivada Foo
e o programa funcionará da mesma maneira.
Eu não sou um programador profissional de POO, mas parece-me que o LSP só é possível se Bar
, derivado de Foo
, não mudar nada nele, mas apenas estendê-lo. Isso significa que, em particular, o programa LSP é verdadeiro somente quando OCP é verdadeiro e OCP é verdadeiro somente se LSP for verdadeiro. Isso significa que eles são iguais.
Corrija-me se eu estiver errado. Eu realmente quero entender essas idéias. Muito obrigado por uma resposta.
Square
deRectangle
não viola o LSP. (Mas é provavelmente ainda má concepção no caso imutável desde que você pode ter quadradosRectangle
s que não são umaSquare
que não correspondem matemática)Respostas:
Puxa, existem alguns conceitos estranhos sobre o que OCP e LSP e alguns são devido à incompatibilidade de algumas terminologias e exemplos confusos. Ambos os princípios são apenas a "mesma coisa" se você os implementar da mesma maneira. Os padrões geralmente seguem os princípios de uma maneira ou de outra, com poucas exceções.
As diferenças serão explicadas mais adiante, mas primeiro vamos mergulhar nos próprios princípios:
Princípio Aberto-Fechado (OCP)
De acordo com o tio Bob :
Observe que a palavra estender neste caso não significa necessariamente que você deve subclassificar a classe real que precisa do novo comportamento. Veja como eu mencionei na primeira incompatibilidade de terminologia? A palavra-chave
extend
significa apenas subclassificação em Java, mas os princípios são mais antigos que Java.O original veio de Bertrand Meyer em 1988:
Aqui é muito mais claro que o princípio é aplicado às entidades de software . Um mau exemplo seria substituir a entidade do software, pois você modifica completamente o código em vez de fornecer algum ponto de extensão. O comportamento da própria entidade de software deve ser extensível e um bom exemplo disso é a implementação do padrão de estratégia (porque é a maneira mais fácil de mostrar o grupo IMHO de padrões GoF):
No exemplo acima, o
Context
está bloqueado para outras modificações. A maioria dos programadores provavelmente gostaria de subclassificar a classe para estendê-la, mas aqui não o fazemos, porque supõe que seu comportamento possa ser alterado através de qualquer coisa que implemente aIBehavior
interface.Ou seja, a classe de contexto está fechada para modificação, mas aberta para extensão . Na verdade, segue outro princípio básico, porque estamos colocando o comportamento na composição de objetos em vez de herança:
Vou deixar o leitor ler esse princípio, pois está fora do escopo desta questão. Para continuar com o exemplo, digamos que temos as seguintes implementações da interface IBehavior:
Usando esse padrão, podemos modificar o comportamento do contexto em tempo de execução, através do
setBehavior
método como ponto de extensão.Portanto, sempre que você desejar estender a classe de contexto "fechada", faça isso subclassificando sua dependência de colaboração "aberta". Claramente, isso não é o mesmo que subclassificar o contexto em si, mas é OCP. O LSP também não menciona isso.
Estendendo com Mixins em vez de herança
Existem outras maneiras de executar o OCP além da subclassificação. Uma maneira é manter suas aulas abertas para extensão através do uso de mixins . Isso é útil, por exemplo, em idiomas baseados em protótipos e não em classes. A idéia é alterar um objeto dinâmico com mais métodos ou atributos, conforme necessário, ou seja, objetos que se misturam ou "se misturam" com outros objetos.
Aqui está um exemplo javascript de um mixin que renderiza um modelo HTML simples para âncoras:
A idéia é estender os objetos dinamicamente e a vantagem disso é que os objetos podem compartilhar métodos, mesmo que estejam em domínios completamente diferentes. No caso acima, você pode criar facilmente outros tipos de âncoras html estendendo sua implementação específica com o
LinkMixin
.Em termos de OCP, os "mixins" são extensões. No exemplo acima,
YoutubeLink
é a nossa entidade de software fechada para modificação, mas aberta para extensões através do uso de mixins. A hierarquia de objetos é achatada, o que impossibilita a verificação de tipos. No entanto, isso não é realmente uma coisa ruim, e explicarei mais adiante que a verificação de tipos geralmente é uma má idéia e a quebra com polimorfismo.Observe que é possível fazer herança múltipla com esse método, pois a maioria das
extend
implementações pode misturar vários objetos:A única coisa que você precisa ter em mente é não colidir os nomes, ou seja, os mixins definem o mesmo nome de alguns atributos ou métodos, pois eles serão substituídos. Na minha humilde experiência, isso não é um problema e, se isso acontecer, é uma indicação de um projeto defeituoso.
Princípio da Substituição de Liskov (LSP)
O tio Bob define-o simplesmente por:
Esse princípio é antigo, na verdade a definição do tio Bob não diferencia os princípios, pois isso faz com que o LSP ainda esteja intimamente relacionado ao OCP pelo fato de que, no exemplo de estratégia acima, o mesmo supertipo é usado (
IBehavior
). Então, vamos analisar sua definição original de Barbara Liskov e ver se podemos descobrir algo mais sobre esse princípio que se parece com um teorema matemático:Vamos encolher os ombros por um tempo, observe que ele não menciona as aulas. No JavaScript, você pode realmente seguir o LSP, mesmo que não seja explicitamente baseado em classe. Se o seu programa tiver uma lista de pelo menos alguns objetos JavaScript que:
... então os objetos são considerados como tendo o mesmo "tipo" e isso realmente não importa para o programa. Isso é essencialmente polimorfismo . No sentido genérico; você não precisa conhecer o subtipo real se estiver usando a interface. OCP não diz nada explícito sobre isso. Na verdade, também identifica um erro de design que muitos programadores iniciantes cometem:
Sempre que você sentir vontade de verificar o subtipo de um objeto, provavelmente está fazendo errado.
Ok, por isso não pode ser errado o tempo todo, mas se você tem o desejo de fazer alguma verificação de tipo com
instanceof
ou enums, você poderia estar fazendo o programa um pouco mais complicado para si mesmo do que ele precisa ser. Mas nem sempre é esse o caso; hacks rápidos e sujos para fazer as coisas funcionarem é uma concessão aceitável para mim, se a solução for pequena o suficiente e se você praticar a refatoração impiedosa , ela poderá melhorar quando as mudanças exigirem.Existem maneiras de contornar esse "erro de design", dependendo do problema real:
Ambos são "erros" comuns no design de código. Existem algumas refatorações diferentes que você pode fazer, como o método pull-up , ou refatorar para um padrão como o padrão Visitor .
Na verdade, eu gosto muito do padrão Visitor, pois ele pode cuidar de espaguete if-statement grande e é mais simples de implementar do que o que você pensaria no código existente. Digamos que tenhamos o seguinte contexto:
Os resultados da instrução if podem ser traduzidos para seus próprios visitantes, pois cada um depende de alguma decisão e código a ser executado. Podemos extrair estes assim:
Nesse ponto, se o programador não soubesse sobre o padrão Visitor, ele implementaria a classe Context para verificar se é de algum tipo específico. Como as classes Visitor têm um
canDo
método booleano , o implementador pode usar essa chamada de método para determinar se é o objeto certo para executar o trabalho. A classe de contexto pode usar todos os visitantes (e adicionar novos) como este:Ambos os padrões seguem OCP e LSP, no entanto, ambos estão identificando coisas diferentes sobre eles. Então, como é o código se viola um dos princípios?
Violar um princípio, mas seguir o outro
Existem maneiras de quebrar um dos princípios, mas ainda assim o outro deve ser seguido. Os exemplos abaixo parecem inventados, por um bom motivo, mas eu já os vi surgindo no código de produção (e até pior):
Segue OCP, mas não LSP
Vamos dizer que temos o código fornecido:
Este pedaço de código segue o princípio de aberto-fechado. Se estivermos chamando o
GetPersons
método do contexto , teremos várias pessoas, todas com suas próprias implementações. Isso significa que o IPerson está fechado para modificação, mas aberto para extensão. No entanto, as coisas mudam quando precisamos usá-lo:Você precisa verificar o tipo e converter o tipo! Lembre-se de como mencionei acima, como a verificação de tipo é uma coisa ruim ? Ah não! Mas não tema, como também mencionado acima, faça alguma refatoração pull-up ou implemente um padrão de Visitante. Nesse caso, podemos simplesmente fazer uma refatoração pull up após adicionar um método geral:
O benefício agora é que você não precisa mais saber o tipo exato, seguindo o LSP:
Segue LSP, mas não OCP
Vamos olhar para algum código que segue o LSP, mas não o OCP, é meio artificial, mas lembre-se de que este é um erro muito sutil:
O código faz LSP porque o contexto pode usar LiskovBase sem conhecer o tipo real. Você acha que esse código também segue o OCP, mas olha atentamente, a classe está realmente fechada ? E se o
doStuff
método fizesse mais do que apenas imprimir uma linha?A resposta se segue o OCP é simplesmente: NÃO , não é porque neste design de objeto somos obrigados a substituir completamente o código por outra coisa. Isso abre a lata de worms recortar e colar, pois é necessário copiar o código da classe base para que as coisas funcionem. O
doStuff
método certamente está aberto para extensão, mas não foi completamente fechado para modificação.Podemos aplicar o padrão do método Template sobre isso. O padrão do método template é tão comum nas estruturas que você pode usá-lo sem conhecê-lo (por exemplo, componentes java swing, c # forms e componentes, etc.). Aqui está uma maneira de fechar o
doStuff
método de modificação e garantir que ele permaneça fechado marcando-o com afinal
palavra-chave java . Essa palavra-chave impede que alguém subclasse a classe ainda mais (em C # você pode usarsealed
para fazer a mesma coisa).Este exemplo segue o OCP e parece bobo, mas imagine isso ampliado com mais código para manipular. Eu continuo vendo o código implementado na produção, onde as subclasses substituem completamente tudo e o código substituído é geralmente cortado e colado entre implementações. Funciona, mas, como em toda duplicação de código, também é uma configuração para pesadelos de manutenção.
Conclusão
Espero que tudo isso esclareça algumas questões sobre OCP e LSP e as diferenças / semelhanças entre eles. É fácil descartá-los da mesma forma, mas os exemplos acima devem mostrar que não são.
Observe que, reunindo o código de exemplo acima:
OCP é sobre bloquear o código ativo, mas ainda assim mantê-lo aberto de alguma forma com algum tipo de ponto de extensão.
Isso é para evitar duplicação de código, encapsulando o código que muda como no exemplo do padrão de Método de Modelo. Ele também permite falhar rapidamente, pois as mudanças de interrupção são dolorosas (ou seja, mude um lugar, quebre em qualquer outro lugar). Por uma questão de manutenção, o conceito de encapsular a mudança é uma coisa boa, porque as mudanças sempre acontecem.
O LSP é permitir que o usuário lide com objetos diferentes que implementam um supertipo sem verificar qual é o tipo real. Isso é inerentemente o polimorfismo .
Esse princípio fornece uma alternativa para verificação e conversão de tipos, que podem sair do controle à medida que o número de tipos aumenta e podem ser alcançadas através da refatoração pull-up ou da aplicação de padrões como o Visitor.
fonte
Isso é algo que causa muita confusão. Prefiro considerar esses princípios um tanto filosoficamente, porque existem muitos exemplos diferentes para eles e, às vezes, exemplos concretos não capturam toda a sua essência.
O que o OCP tenta corrigir
Digamos que precisamos adicionar funcionalidade a um determinado programa. A maneira mais fácil de fazer isso, especialmente para pessoas treinadas para pensar procedimentalmente, é adicionar uma cláusula if sempre que necessário, ou algo do tipo.
Os problemas com isso são
Você pode fazer isso adicionando um campo adicional a todos os livros chamados "is_on_sale" e, em seguida, pode verificar esse campo ao imprimir o preço de qualquer livro ou , alternativamente , pode instanciar livros à venda no banco de dados usando um tipo diferente, que imprime "(À VENDA)" na cadeia de preços (não é um design perfeito, mas oferece o ponto inicial).
O problema com a primeira solução processual é um campo extra para cada livro e complexidade redundante extra em muitos casos. A segunda solução força apenas a lógica onde é realmente necessária.
Agora considere o fato de que pode haver muitos casos em que dados e lógicos diferentes são necessários, e você verá por que é uma boa idéia manter em mente o OCP ao projetar suas classes ou reagir a mudanças nos requisitos.
Agora você deve ter a idéia principal: tente se colocar em uma situação em que o novo código possa ser implementado como extensões polimórficas, não como modificações de procedimentos.
Mas nunca tenha medo de analisar o contexto e ver se as desvantagens superam os benefícios, porque mesmo um princípio como o OCP pode causar uma confusão de 20 classes em um programa de 20 linhas, se não for tratado com cuidado .
O que o LSP tenta corrigir
Todos nós amamos a reutilização de código. Uma doença que se segue é que muitos programas não a entendem completamente, a ponto de fatorarem cegamente linhas de código comuns apenas para criar complexidades ilegíveis e acoplamentos redundantes entre módulos que, além de algumas linhas de código, não tem nada em comum no que diz respeito ao trabalho conceitual a ser realizado.
O maior exemplo disso é a reutilização da interface . Você provavelmente já presenciou; uma classe implementa uma interface, não porque seja uma implementação lógica dela (ou uma extensão no caso de classes base concretas), mas porque os métodos que ela declara naquele momento têm as assinaturas corretas no que diz respeito.
Mas então você encontra um problema. Se as classes implementarem interfaces apenas considerando as assinaturas dos métodos declarados, você poderá passar instâncias de classes de uma funcionalidade conceitual para lugares que exigem funcionalidades completamente diferentes, que dependem apenas de assinaturas semelhantes.
Isso não é tão horrível, mas causa muita confusão, e temos a tecnologia para nos impedir de cometer erros como esses. O que precisamos fazer é tratar as interfaces como API + Protocol . A API é aparente nas declarações e o protocolo é aparente nos usos existentes da interface. Se tivermos 2 protocolos conceituais que compartilham a mesma API, eles deverão ser representados como 2 interfaces diferentes. Caso contrário, ficaremos presos ao dogmatismo DRY e, ironicamente, apenas criaremos código mais difícil de manter.
Agora você deve entender a definição perfeitamente. O LSP diz: Não herde de uma classe base e implemente funcionalidade nessas subclasses que, em outros locais, que dependem da classe base, não se dão bem.
fonte
Pelo meu entendimento:
OCP diz: "Se você adicionar novas funcionalidades, crie uma nova classe estendendo uma existente, em vez de alterá-la".
O LSP diz: "Se você criar uma nova classe estendendo uma classe existente, verifique se ela é totalmente intercambiável com sua base".
Então eu acho que eles se complementam, mas não são iguais.
fonte
Embora seja verdade que o OCP e o LSP estejam relacionados à modificação, o tipo de modificação sobre o qual o OCP fala não é o que o LSP fala.
Modificar em relação ao OCP é a ação física de um desenvolvedor escrevendo código em uma classe existente.
O LSP lida com a modificação de comportamento que uma classe derivada traz em comparação com a classe base e a alteração do tempo de execução da execução do programa que pode ser causada pelo uso da subclasse em vez da superclasse.
Portanto, embora possam parecer semelhantes à distância, OCP! = LSP. Na verdade, acho que eles podem ser os únicos 2 princípios do SOLID que não podem ser entendidos em termos um do outro.
fonte
Isto está errado. O LSP afirma que a classe Bar não deve introduzir comportamento, o que não é esperado quando o código usa Foo, quando Bar é derivado do Foo. Não tem nada a ver com perda de funcionalidade. Você pode remover a funcionalidade, mas somente quando o código usando Foo não depende dessa funcionalidade.
Mas no final, isso geralmente é difícil de alcançar, porque na maioria das vezes, o código usando Foo depende de todo o seu comportamento. Portanto, removê-lo viola o LSP. Mas simplificar dessa maneira é apenas parte do LSP.
fonte
Sobre objetos que podem violar
Para entender a diferença, você deve entender os assuntos de ambos os princípios. Não é uma parte abstrata do código ou situação que pode violar ou não algum princípio. Sempre são alguns componentes específicos - função, classe ou módulo - que podem violar o OCP ou LSP.
Quem pode violar o LSP
Pode-se verificar se o LSP está quebrado apenas quando há uma interface com algum contrato e uma implementação dessa interface. Se a implementação não estiver em conformidade com a interface ou, de um modo geral, com o contrato, o LSP será quebrado.
Exemplo mais simples:
O contrato afirma claramente que
addObject
deve anexar seu argumento ao contêiner. ECustomContainer
claramente quebra esse contrato. Assim, aCustomContainer.addObject
função viola o LSP. Assim, aCustomContainer
classe viola o LSP. A consequência mais importante é queCustomContainer
não pode ser passada parafillWithRandomNumbers()
.Container
não pode ser substituído porCustomContainer
.Lembre-se de um ponto muito importante. Não é esse código inteiro que quebra o LSP, é específica
CustomContainer.addObject
e geralmenteCustomContainer
que quebra o LSP. Quando você declara que o LSP foi violado, sempre deve especificar duas coisas:É isso aí. Apenas um contrato e sua implementação. Um downcast no código não diz nada sobre violação de LSP.
Quem pode violar o OCP
Pode-se verificar se o OCP é violado apenas quando há um conjunto de dados limitado e um componente que manipula valores desse conjunto de dados. Se os limites do conjunto de dados puderem mudar com o tempo e isso exigir a alteração do código-fonte do componente, o componente violará o OCP.
Parece complexo. Vamos tentar um exemplo simples:
O conjunto de dados é o conjunto de plataformas suportadas.
PlatformDescriber
é o componente que manipula valores desse conjunto de dados. Adicionar uma nova plataforma requer a atualização do código fonte dePlatformDescriber
. Assim, aPlatformDescriber
classe viola o OCP.Outro exemplo:
O "conjunto de dados" é o conjunto de canais em que uma entrada de log deve ser adicionada.
Logger
é o componente responsável por adicionar entradas a todos os canais. Adicionar suporte para outra maneira de registrar exige a atualização do código fonte deLogger
. Assim, aLogger
classe viola o OCP.Observe que nos dois exemplos o conjunto de dados não é algo semanticamente corrigido. Isso pode mudar com o tempo. Uma nova plataforma pode surgir. Um novo canal de log pode surgir. Se o seu componente precisar ser atualizado quando isso acontecer, ele violará o OCP.
Forçando os limites
Agora a parte complicada. Compare os exemplos acima com o seguinte:
Você pode pensar que
translateToRussian
viola o OCP. Mas na verdade não é.GregorianWeekDay
tem um limite específico de exatamente 7 dias da semana com nomes exatos. E o importante é que esses limites semanticamente não possam mudar ao longo do tempo. Sempre haverá 7 dias na semana gregoriana. Sempre haverá segunda, terça-feira etc. Este conjunto de dados é semanticamente corrigido. Não é possível quetranslateToRussian
o código fonte exija modificações. Portanto, o OCP não é violado.Agora deve ficar claro que uma
switch
declaração exaustiva nem sempre é uma indicação de OCP quebrado.A diferença
Agora sinta a diferença:
Essas condições são completamente ortogonais.
Exemplos
Na resposta de @ Spoike, o princípio Violar um, mas seguir a outra parte, está totalmente errado.
No primeiro exemplo, a
for
parte -loop está claramente violando o OCP porque não é extensível sem modificação. Mas não há indicação de violação do LSP. E nem está claro se oContext
contrato permite que a getPersons retorne algo, excetoBoss
ouPeon
. Mesmo assumindo um contrato que permita oIPerson
retorno de qualquer subclasse, não há classe que substitua essa condição posterior e a viole. Além disso, se getPersons retornar uma instância de alguma terceira classe, ofor
-loop fará seu trabalho sem falhas. Mas esse fato não tem nada a ver com LSP.Próximo. No segundo exemplo, nem o LSP nem o OCP são violados. Novamente, a
Context
parte simplesmente não tem nada a ver com LSP - sem contrato definido, sem subclassificação, sem interrupções. Não éContext
quem deve obedecer ao LSP, éLiskovSub
não deve quebrar o contrato de sua base. Em relação à OCP, a turma está realmente fechada? - sim. Nenhuma modificação é necessária para estendê-la. Obviamente, o nome do ponto de extensão indica Faça o que quiser, sem limites . O exemplo não é muito útil na vida real, mas claramente não viola o OCP.Vamos tentar fazer alguns exemplos corretos com verdadeira violação do OCP ou LSP.
Siga o OCP, mas não o LSP
Aqui,
HumanReadablePlatformSerializer
não requer modificações quando uma nova plataforma é adicionada. Assim segue o OCP.Mas o contrato exige que
toJson
deve retornar um JSON formatado corretamente. A turma não faz isso. Por esse motivo, não pode ser transmitido para um componente usadoPlatformSerializer
para formatar o corpo de uma solicitação de rede. Assim,HumanReadablePlatformSerializer
viola o LSP.Siga o LSP, mas não o OCP
Algumas modificações no exemplo anterior:
O serializador retorna a string JSON formatada corretamente. Portanto, não há violação de LSP aqui.
Mas há um requisito de que, se a plataforma for mais amplamente usada, deve haver indicação correspondente no JSON. Neste exemplo, o OCP é violado por
HumanReadablePlatformSerializer.isMostPopular
função porque um dia o iOS se torna a plataforma mais popular. Formalmente, isso significa que o conjunto de plataformas mais usadas é definido como "Android" por enquanto eisMostPopular
manipula inadequadamente esse conjunto de dados. O conjunto de dados não é semanticamente fixo e pode mudar livremente ao longo do tempo.HumanReadablePlatformSerializer
É necessário atualizar o código fonte do caso em caso de alteração.Você também pode observar uma violação da responsabilidade única neste exemplo. Eu intencionalmente fiz isso para poder demonstrar os dois princípios na mesma entidade. Para corrigir o SRP, você pode extrair a
isMostPopular
função para algum externoHelper
e adicionar um parâmetro aPlatformSerializer.toJson
. Mas isso é outra história.fonte
LSP e OCP não são os mesmos.
O LSP fala sobre a correção do programa como está . Se uma instância de um subtipo interromper a correção do programa quando substituída no código por tipos de ancestrais, você demonstrou uma violação do LSP. Pode ser necessário simular um teste para mostrar isso, mas não é necessário alterar a base de código subjacente. Você está validando o próprio programa para ver se ele atende ao LSP.
O OCP fala sobre a correção das alterações no código do programa, o delta de uma versão de origem para outra. O comportamento não deve ser modificado. Só deve ser estendido. O exemplo clássico é adição de campo. Todos os campos existentes continuam a operar como antes. O novo campo apenas adiciona funcionalidade. A exclusão de um campo, no entanto, geralmente é uma violação do OCP. Aqui você está validando o delta da versão do programa para verificar se ele atende ao OCP.
Então essa é a principal diferença entre LSP e OCP. O primeiro valida apenas a base de código como está , o último valida apenas o delta da base de código de uma versão para a seguinte . Como tal, eles não podem ser a mesma coisa, são definidos como validando coisas diferentes.
Darei a você uma prova mais formal: dizer "LSP implica OCP" implicaria um delta (porque o OCP exige um outro que não seja o caso trivial), mas o LSP não exige um. Então isso é claramente falso. Por outro lado, podemos refutar "OCP implica LSP" simplesmente dizendo que OCP é uma declaração sobre deltas; portanto, não diz nada sobre uma declaração sobre um programa no local. Isso decorre do fato de que você pode criar QUALQUER delta começando com QUALQUER programa em vigor. Eles são totalmente independentes.
fonte
Eu olhava para ele do ponto de vista do cliente. se o Cliente estiver usando recursos de uma interface e internamente esse recurso foi implementado pela Classe A. Suponha que haja uma classe B que estenda a classe A, amanhã, se eu remover a classe A dessa interface e colocar a classe B, a classe B deverá também fornece os mesmos recursos para o cliente. O exemplo padrão é uma classe Duck que nada e, se o ToyDuck estender o Duck, ele também deve nadar e não reclama que não sabe nadar; caso contrário, o ToyDuck não deveria ter estendido a classe Duck.
fonte