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 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:
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 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 a IBehavior
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:
"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 setBehavior
mé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 extend
implementaçõ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 o1
do tipo S
não é um objeto o2
do tipo T
de tal forma que para todos os programas P
definidos em termos de T
, o comportamento de P
nã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 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:
- 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 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:
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 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:
// 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 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 a final
palavra-chave java . Essa palavra-chave impede que alguém subclasse a classe ainda mais (em C # você pode usar sealed
para 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.