LSP vs OCP / Liskov Substitution VS Open Fechar


48

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 Foopode ser substituída por qualquer instância da Barqual é derivada Fooe 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.


4
Essa é uma interpretação muito restrita dos dois conceitos. Aberto / fechado pode ser mantido e ainda assim violar o LSP. Os exemplos Retângulo / Quadrado ou Elipse / Círculo são boas ilustrações. Ambos aderem ao OCP, mas ambos violam o LSP.
Joel Etherton

11
O mundo (ou pelo menos a internet) está confuso sobre isso. kirkk.com/modularity/2009/12/solid-principles-of-class-design . Esse cara diz que a violação do LSP também é violação do OCP. E então, no livro "Design de engenharia de software: Teoria e prática" na página 156, o autor dá um exemplo de algo que adere ao OCP, mas viola o LSP. Eu desisti disso.
Dilsh R

@JoelEtherton Esses pares só violam o LSP se forem mutáveis. No caso imutável, derivar Squarede Rectanglenão viola o LSP. (Mas é provavelmente ainda má concepção no caso imutável desde que você pode ter quadrados Rectangles que não são uma Squareque não correspondem matemática)
CodesInChaos

Analogia simples (do ponto de vista de um escritor de biblioteca-usuário). O LSP é como vender um produto (biblioteca) que afirma implementar 100% do que diz (na interface ou no manual do usuário), mas na verdade não corresponde (ou não corresponde ao que foi dito). OCP é como vender um produto (biblioteca) com a promessa de que ele pode ser atualizado (estendido) quando surgir uma nova funcionalidade (como firmware), mas na verdade não pode ser atualizado sem um serviço de fábrica.
rwong

Respostas:


119

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 :

Você deve poder estender um comportamento de classe, sem modificá-lo.

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 extendsignifica apenas subclassificação em Java, mas os princípios são mais antigos que Java.

O original veio de Bertrand Meyer em 1988:

As entidades de software (classes, módulos, funções etc.) devem estar abertas para extensão, mas fechadas para modificação.

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):

// Context is closed for modifications. Meaning you are
// not supposed to change the code here.
public class Context {

    // Context is however open for extension through
    // this private field
    private IBehavior behavior;

    // The context calls the behavior in this public 
    // method. If you want to change this you need
    // to implement it in the IBehavior object
    public void doStuff() {
        if (this.behavior != null)
            this.behavior.doStuff();
    }

    // You can dynamically set a new behavior at will
    public void setBehavior(IBehavior behavior) {
        this.behavior = behavior;
    }
}

// The extension point looks like this and can be
// subclassed/implemented
public interface IBehavior {
    public void doStuff();
}

No exemplo acima, o Contextestá 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 a IBehaviorinterface.

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:

"Favorecer ' composição de objeto ' sobre ' herança de classe '." (Gangue dos Quatro 1995: 20)

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:

public class HelloWorldBehavior implements IBehavior {
    public void doStuff() {
        System.println("Hello world!");
    }
}

public class GoodByeBehavior implements IBehavior {
    public void doStuff() {
        System.out.println("Good bye cruel world!");
    }
}

Usando esse padrão, podemos modificar o comportamento do contexto em tempo de execução, através do setBehaviormétodo como ponto de extensão.

// in your main method
Context c = new Context();

c.setBehavior(new HelloWorldBehavior());
c.doStuff();
// prints out "Hello world!"

c.setBehavior(new GoodByeBehavior());
c.doStuff();
// prints out "Good bye cruel world!"

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:

// The mixin, provides a template for anchor HTML elements, i.e. <a>
var LinkMixin = {
    render: function() {
        return '<a href="' + this.link +'">'
            + this.content 
            + '</a>;
    }
}

// Constructor for a youtube link
var YoutubeLink = function(content, youtubeId) {
    this.content = content;
    this.setLink(this.youtubeId);
};
// Methods are added to the prototype
YoutubeLink.prototype = {
    setLink: function(youtubeid) {
        this.link = 'http://www.youtube.com/watch?v=' + youtubeid;
    }
};
// Extend YoutubeLink prototype with the LinkMixin using
// underscore/lodash extend
_.extend(YoutubeLink.protoype, LinkMixin);

// When used:
var ytLink = new YoutubeLink("Cool Movie!", "idOaZpX8lnA");

console.log(ytLink.render());
// will output: 
// <a href="http://www.youtube.com/watch?=vidOaZpX8lnA">Cool Movie!</a>

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 extendimplementações pode misturar vários objetos:

_.extend(MyClass, Mixin1, Mixin2 /* [, ...] */);

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:

Classes derivadas devem ser substituíveis por suas classes base.

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:

O que se quer aqui é algo como a seguinte propriedade de substituição: Se para cada objeto o1do tipo Snão é um objeto o2do tipo Tde tal forma que para todos os programas Pdefinidos em termos de T, o comportamento de Pnão se altera quando o1é substituído o2, em seguida, Sé um subtipo de T.

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:

  • precisa ser calculado da mesma maneira,
  • tem o mesmo comportamento e
  • de outra forma, são completamente diferentes

... 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 instanceofou 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:

  • A superclasse não está chamando os pré-requisitos, forçando o chamador a fazê-lo.
  • Falta à superclasse um método genérico necessário ao chamador.

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:

public class Context {

    public void doStuff(string query) {

        // outcome no. 1
        if (query.Equals("Hello")) {
            System.out.println("Hello world!");
        } 

        // outcome no. 2
        else if (query.Equals("Bye")) {
            System.out.println("Good bye cruel world!");
        }

        // a change request may require another outcome...

    }

}

// usage:
Context c = new Context();

c.doStuff("Hello");
// prints "Hello world"

c.doStuff("Bye");
// prints "Bye"

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:

public interface IVisitor {
    public bool canDo(string query);
    public void doStuff();
}

// outcome 1
public class HelloVisitor implements IVisitor {
    public bool canDo(string query) {
        return query.Equals("Hello");
    }
    public void doStuff() {
         System.out.println("Hello World");
    }
}

// outcome 2
public class ByeVisitor implements IVisitor {
    public bool canDo(string query) {
        return query.Equals("Bye");
    }
    public void doStuff() {
        System.out.println("Good bye cruel world");
    }
}

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 canDomé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:

public class Context {
    private ArrayList<IVisitor> visitors = new ArrayList<IVisitor>();

    public Context() {
        visitors.add(new HelloVisitor());
        visitors.add(new ByeVisitor());
    }

    // instead of if-statements, go through all visitors
    // and use the canDo method to determine if the 
    // visitor object is the right one to "visit"
    public void doStuff(string query) {
        for(IVisitor visitor : visitors) {
            if (visitor.canDo(query)) {
                visitor.doStuff();
                break;
                // or return... it depends if you have logic 
                // after this foreach loop
            }
        }
    }

    // dynamically adds new visitors
    public void addVisitor(IVisitor visitor) {
        if (visitor != null)
            visitors.add(visitor);
    }
}

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:

public interface IPerson {}

public class Boss implements IPerson {
    public void doBossStuff() { ... }
}

public class Peon implements IPerson {
    public void doPeonStuff() { ... }
}

public class Context {
    public Collection<IPerson> getPersons() { ... }
}

Este pedaço de código segue o princípio de aberto-fechado. Se estivermos chamando o GetPersonsmé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:

// in some routine that needs to do stuff with 
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
    // now we have to check the type... :-P
    if (person instanceof Boss) {
        ((Boss) person).doBossStuff();
    }
    else if (person instanceof Peon) {
        ((Peon) person).doPeonStuff();
    }
}

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:

public class Boss implements IPerson {
    // we're adding this general method
    public void doStuff() {
        // that does the call instead
        this.doBossStuff();
    }
    public void doBossStuff() { ... }
}


public interface IPerson {
    // pulled up method from Boss
    public void doStuff();
}

// do the same for Peon

O benefício agora é que você não precisa mais saber o tipo exato, seguindo o LSP:

// in some routine that needs to do stuff with 
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
    // yay, no type checking!
    person.doStuff();
}

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:

public class LiskovBase {
    public void doStuff() {
        System.out.println("My name is Liskov");
    }
}

public class LiskovSub extends LiskovBase {
    public void doStuff() {
        System.out.println("I'm a sub Liskov!");
    }
}

public class Context {
    private LiskovBase base;

    // the good stuff
    public void doLiskovyStuff() {
        base.doStuff();
    }

    public void setBase(LiskovBase base) { this.base = base }
}

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 doStuffmé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 doStuffmé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 doStuffmétodo de modificação e garantir que ele permaneça fechado marcando-o com a finalpalavra-chave java . Essa palavra-chave impede que alguém subclasse a classe ainda mais (em C # você pode usar sealedpara fazer a mesma coisa).

public class LiskovBase {
    // this is now a template method
    // the code that was duplicated
    public final void doStuff() {
        System.out.println(getStuffString());
    }

    // extension point, the code that "varies"
    // in LiskovBase and it's subclasses
    // called by the template method above
    // we expect it to be virtual and overridden
    public string getStuffString() {
        return "My name is Liskov";
    }
}

public class LiskovSub extends LiskovBase {
    // the extension overridden
    // the actual code that varied
    public string getStuffString() {
        return "I'm sub Liskov!";
    }
}

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.


7
Essa é uma boa explicação, porque não simplifica demais o OCP, ao implicar que sempre significa implementação por herança. É essa simplificação excessiva que une OCP e SRP na mente de algumas pessoas, quando na verdade elas podem ser dois conceitos completamente separados.
Eric Rei

5
Esta é uma das melhores respostas de troca de pilhas que eu já vi. Eu gostaria de poder votar 10 vezes. Muito bem, e obrigado pela excelente explicação.
Bob Horn

Lá, eu adicionei um resumo do Javascript que não é uma linguagem de programação baseada em classe, mas ainda pode seguir o LSP e editar o texto para que, com sorte, ele seja lido com mais fluência. Ufa!
Spoike

Embora sua citação do tio Bob do LSP esteja correta (igual ao site dele), não deveria ser o contrário? Não deveria declarar que "As classes base devem ser substituíveis por suas classes derivadas"? No LSP, o teste de "compatibilidade" é feito em relação à classe derivada e não à base. Ainda assim, não sou um falante nativo de inglês e acho que pode haver alguns detalhes sobre a frase que talvez esteja faltando.
Alpha

@ Alpha: Essa é uma boa pergunta. A classe base é sempre substituível pelas classes derivadas, caso contrário, a herança não funcionaria. O compilador (pelo menos em Java e C #) reclamará se você estiver deixando de fora um membro (método ou atributo / campo) da classe estendida que precisa ser implementada. O LSP destina-se a impedir que você adicione métodos que estão disponíveis apenas localmente nas classes derivadas, pois isso exige que o usuário dessas classes derivadas saiba sobre eles. À medida que o código cresce, esses métodos seriam difíceis de manter.
Spoike

15

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

  1. Ele altera o fluxo do código de trabalho existente.
  2. Isso força uma nova ramificação condicional em todos os casos. Por exemplo, digamos que você tenha uma lista de livros e alguns deles estejam à venda e deseje iterar sobre todos eles e imprimir o preço, de modo que, se estiverem à venda, o preço impresso incluirá a sequência " (À VENDA)".

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.


11
Eu me inscrevi apenas para poder votar nas respostas de Spoike - ótimo trabalho.
David Culp

7

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.


4

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.


2

O LSP em palavras simples afirma que qualquer instância do Foo pode ser substituída por qualquer instância do Bar derivada do Foo sem perda de funcionalidade do programa.

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.


Um caso muito comum é onde o objeto substituído remove efeitos colaterais : por exemplo. um logger fictício que não produz nada ou um objeto falso usado no teste.
Inutil

0

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:

class Container {
    // Should add the object to the container.
    void addObject(object) {
        internalArray.append(object);
    }

    int size() {
        return internalArray.size();
    }
}

class CustomContainer extends Container {
    @Override void addObject(object) {
        System.console.print("Skipping object! Ha-ha!");
    }
}

void fillWithRandomNumbers(Container container) {
    while (container.size() < 42) {
        container.addObject(Randomizer.getNumber())
    }
}

O contrato afirma claramente que addObjectdeve anexar seu argumento ao contêiner. E CustomContainerclaramente quebra esse contrato. Assim, a CustomContainer.addObjectfunção viola o LSP. Assim, a CustomContainerclasse viola o LSP. A consequência mais importante é que CustomContainernão pode ser passada para fillWithRandomNumbers(). Containernão pode ser substituído por CustomContainer.

Lembre-se de um ponto muito importante. Não é esse código inteiro que quebra o LSP, é específica CustomContainer.addObjecte geralmente CustomContainerque quebra o LSP. Quando você declara que o LSP foi violado, sempre deve especificar duas coisas:

  • A entidade que viola o LSP.
  • O contrato que é quebrado pela entidade.

É 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:

enum Platform {
    iOS,
    Android
}

class PlatformDescriber {
    String describe(Platform platform) {
        switch (platform) {
            case iOS: return "iPhone OS, v10.0.1";
            case Android: return "Android, v7.1";
        }
    }
}

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 de PlatformDescriber. Assim, a PlatformDescriberclasse viola o OCP.

Outro exemplo:

class Shop {
    void sellItemToCustomer(item, customer) {
        // some buisiness logic here
        ...
        logger.logItemSold()
    }
}

class Logger {
    void logItemSold() {
        logger.logToStdErr("an item was sold")
        logger.logToRemote("an item was sold")
        logger.logToDatabase("an item was sold")
    }
}

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 de Logger. Assim, a Loggerclasse 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:

enum GregorianWeekDay {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
}

String translateToRussian(GregorianWeekDay weekDay) {
    switch (weekDay) {
        case Monday: return "Понедельник";
        case Tuesday: return "Вторник";
        case Wednesday: return "Среда";
        case Thursday: return "Четверг";
        case Friday: return "Пятница";
        case Saturday: return "Суббота";
        case Sunday: return "Воскресенье";
    }
}

Você pode pensar que translateToRussianviola o OCP. Mas na verdade não é. GregorianWeekDaytem 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 que translateToRussiano código fonte exija modificações. Portanto, o OCP não é violado.

Agora deve ficar claro que uma switchdeclaração exaustiva nem sempre é uma indicação de OCP quebrado.

A diferença

Agora sinta a diferença:

  • O assunto do LSP é "uma implementação de interface / contrato". Se a implementação não estiver em conformidade com o contrato, ela quebrará o LSP. Não é importante se essa implementação pode mudar com o tempo ou não, se é extensível ou não.
  • O assunto do OCP é "uma maneira de responder a uma alteração de requisitos". Se o suporte a um novo tipo de dados exigir a alteração do código-fonte do componente que lida com esses dados, esse componente interromperá o OCP. Não é importante se o componente quebra seu contrato ou não.

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 forparte -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 o Contextcontrato permite que a getPersons retorne algo, exceto Bossou Peon. Mesmo assumindo um contrato que permita o IPersonretorno 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, o for-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 Contextparte simplesmente não tem nada a ver com LSP - sem contrato definido, sem subclassificação, sem interrupções. Não é Contextquem deve obedecer ao LSP, é LiskovSubnã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

interface Platform {
    String name();
    String version();
}

class iOS implements Platform {
    @Override String name() { return "iOS"; }
    @Override String version() { return "10.0.1"; }
}

interface PlatformSerializer {
    String toJson(Platform platform);
}

class HumanReadablePlatformSerializer implements PlatformSerializer {
    String toJson(Platform platform) {
        return platform.name() + ", v" + platform.version();
    }
}

Aqui, HumanReadablePlatformSerializernão requer modificações quando uma nova plataforma é adicionada. Assim segue o OCP.

Mas o contrato exige que toJsondeve retornar um JSON formatado corretamente. A turma não faz isso. Por esse motivo, não pode ser transmitido para um componente usado PlatformSerializerpara formatar o corpo de uma solicitação de rede. Assim, HumanReadablePlatformSerializerviola o LSP.

Siga o LSP, mas não o OCP

Algumas modificações no exemplo anterior:

class Android implements Platform {
    @Override String name() { return "Android"; }
    @Override String version() { return "7.1"; }
}
class HumanReadablePlatformSerializer implements PlatformSerializer {
    String toJson(Platform platform) {
        return "{ "
                + "\"name\": \"" + platform.name() + "\","
                + "\"version\": \"" + platform.version() + "\","
                + "\"most-popular\": " + isMostPopular(platform) + ","
                + "}"
    }

    boolean isMostPopular(Platform platform) {
        return (platform instanceof Android)
    }
}

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.isMostPopularfunçã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 e isMostPopularmanipula 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 isMostPopularfunção para algum externo Helpere adicionar um parâmetro a PlatformSerializer.toJson. Mas isso é outra história.


0

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.


-1

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.


Seria muito construtivo se as pessoas também comentassem enquanto votavam em alguma resposta. Afinal, estamos todos aqui para compartilhar conhecimento, e simplesmente julgar sem razão adequada não servirá a nenhum propósito.
AKS

este não parece oferecer nada substancial sobre pontos feitos e explicado em anteriores 6 respostas
mosquito

11
Parece que você está apenas explicando um dos princípios, o L que eu acho. Para o que está tudo bem, mas a pergunta pediu uma comparação / contraste de dois princípios diferentes. Provavelmente é por isso que alguém votou contra.
StarWeaver 23/09
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.