Olá pessoas, através desse post eu estou dando inicio à uma série onde eu explico um pouco sobre os principais design patterns usados no mercado de uma forma que seja de fácil entendimento à qualquer desenvolvedor, então espero que gostem :)
Minha principal fonte será o livro Head First Design Patterns: Building Extensible and Maintainable Object-Oriented Software, que inclusive é de onde eu tirei o título para esse post, então meu objetivo aqui é fazer um resumo dos principais pontos que o livro trata e o que podemos aprender com tudo isso. Caso tenha interesse, recomendo muito lê-lo, ele aborda os padrões de projeto de uma forma muito didática e divertida.
Os design patterns são como ferramentas no cinto de um carpinteiro, estratégias testadas e aprovadas para lidar com problemas específicos. Em vez de reinventar a roda a cada projeto, podemos aprender com as melhores práticas consolidadas pela comunidade de desenvolvimento.
Primeiramente, a cada pattern explicado, irei colocar a lista dos Princípios de Design de código que são citados no livro, pois eles que permeiam todo o conceito dos Design Patterns. Além disso, irei demonstrar com qual(is) desses princípios o padrão atual se relaciona.
Princípios de design de código
- Encapsule o que varia.
- Prefira composição à herança.
- Programe para interfaces, não implementações.
- Se esforce em usar designs que tenham fraco acoplamento entre objetos que interagem entre si.
- Princípio do menor conhecimento: Apenas fale com seus amigos.
- Princípio de Hollywood: Não nos chame, nós chamamos você.
- Uma classe deve haver apenas uma razão para mudar.
Strategy
Definição
O padrão de projeto Strategy define uma família de algoritmos, encapsula cada um deles, e os permitem serem intercambiáveis (trocados) em tempo de execução. O padrão deixa o algoritmo variar independentemente dos clientes que o usam.
Problemática - Jogo de Patos
Esse é um dos exemplos mais clássicos ao se falar de Strategy, mas é bem didático e fácil de entender, então vamos lá.
Vamos imaginar que estamos desenvolvendo um jogo onde o usuário pode controlar um pato. Esse pato pode nadar e fazer quack. Então, vamos criar uma classe Duck
que será a classe base para todos os patos do jogo. Essa classe terá os métodos swim()
, quack()
e display()
, que mostra o pato na tela.
public abstract class Duck {
public void swim() {
System.out.println("I'm swimming!");
}
public void quack() {
System.out.println("Quack!");
}
// Cada pato tem que sobreescrever o método
public abstract void display();
}
Também precisamos dos patos em si, então vamos criar as classes MallardDuck
e RedheadDuck
que herdam de Duck
.
Mas, após um tempo, decidiram que o jogo precisava de patos voadores.
Qual é o primeira solução que vem à mente? "Ah, vou criar um método fly()
na classe Duck
e pronto, agora os patos podem voar!".
Então ok, vamos adicionar o método fly()
.
Mas temos um problema...
Depois de um tempo, decidiram que o jogo precisava de patos de borracha. E agora? Patos de borracha não voam, então não faz sentido eles terem o método fly()
. O que faremos? Vamos criar uma classe RubberDuck
que herda de Duck
e sobrescreve o método fly()
para não fazer nada? Isso pode até funcionar para esse caso, mas e se precisarem de outro tipo de pato que não voa e nem faz quack (um pato de madeira, por exemplo)? Iremos sobrescrever todos os métodos que não fazem sentido para ele e deixar como nulo? Dessa forma só estamos poluindo nossas classes e não estamos utilizando a herança de maneira efetiva.
E agora?
E agora temos um problema, pois esse é apenas um dos casos em que o design aplicado não é flexível o suficiente para lidar com qualquer tipo de pato. Podem haver outros tipos de patos que precisem de outros métodos diferentes, ou que precisem apenas parcialmente dos métodos.
Então precisamos pensar em uma forma de deixar o código mais flexível.
Problemas na utilização de herança nos seus designs
Uma das primeiras coisas que a gente aprende na faculdade/técnico, é a chamada herança. Ela é um dos pilares da Programação Orientada a Objetos, porém, em sua grande maioria, não é a melhor solução para os problemas que encontramos no dia a dia. Ela nos deixa preso à classe pai, então sempre que pensar em usar herança, pense duas vezes, pois ela pode não ser a melhor solução.
Algumas das desvantagens da herança no nosso exemplo são:
- Herança é definida tempo de compilação: Isso significa que não podemos alterar o comportamento de um objeto em tempo de execução, pois ele já foi definido em tempo de compilação.
- Mudanças podem afetar outros patos sem querer.
Isso não quer dizer que a herança seja sempre ruim, mas em quase todos os Design Patterns que veremos nessa série, usamos de composição ao invés de herança, pois ela nos dá mais flexibilidade e nos permite alterar o comportamento dos objetos em tempo de execução.
Solução - Strategy
Antes de irmos para a solução, lembram do primeiro princípio de design? "Encapsule o que varia", ou mais especificamente, "Identifique os aspectos da sua aplicação que variem e os separe daquilo que permanece o mesmo". Então, iremos usar dele como base para nossa solução.
Bom, o que varia? O comportamento dos patos, tais como voar e quack. Então, vamos encapsular esses comportamentos em classes separadas e fazer com que os patos tenham uma referência para esses comportamentos. Dessa forma, podemos alterá-los em tempo de execução.
Para separarmos os comportamentos, usaremos o terceiro princípio de design: "Programe para interfaces, não implementações". Pois dessa forma, cada tipo de comportamento implementará sua respectiva interface, sendo possível mudar entre implementações a qualquer momento, já que o pato só precisa saber que ele tem um comportamento e não como ele funciona.
Obs: Lembrando que "interfaces" no nosso contexto não significa necessariamente apenas interfaces das linguagens orientadas a objetos, mas sim, qualquer supertipo que nos permita referenciá-lo sem precisar saber qual é o o tipo exato do objeto. Em java, por exemplo: Classe abstrata, interfaces, etc.
Interface FlyBehavior
Interface que será usada para definir o tipo de vôo do pato, para cada tipo de vôo, uma classe nova será criada.
Interface QuackBehavior
Interface que será usada para definir o tipo de "quack" do pato, para cada tipo de quack, uma classe nova será criada.
Como integrar isso com nossa classe Duck?
O segredo aqui é fazer a classe Duck
delegar seus comportamentos de voar e fazer quack para as interfaces criadas acima.
Então, seguiremos alguns passos na classe Duck
:
- Adicionar dois atributos (do tipo interface) que armazenam ambos comportamentos.
- Trocar métodos
quack()
efly()
para novos que apenas chamam oquack()
efly()
dos comportamentos.
public abstract class Duck {
protected FlyBehavior flyBehavior;
protected QuackBehavior quackBehavior;
public Duck() {}
public abstract void display();
public void performFly() {
flyBehavior.fly(); // Delegou para o flyBehavior
}
public void performQuack() {
quackBehavior.quack(); // Delegou para o quackBehavior
}
public void swim() {
System.out.println("Nadar");
}
}
Classes após Strategy
public class FlyWithWings implements FlyBehavior {
@Override
public void fly() {
System.out.println("FlyWithWings (voa com asas)");
}
}
public class FlyNoWay implements FlyBehavior {
@Override
public void fly() {
System.out.println("FlyNoWay (não consegue voar)");
}
}
public class Quack implements QuackBehavior {
@Override
public void quack() {
System.out.println("Quack");
}
}
public class MallardDuck extends Duck {
public MallardDuck() {
quackBehavior = new Quack();
flyBehavior = new FlyWithWings();
}
public void display() {
System.out.println("MallardDuck");
}
}
Agora, para cada tipo de pato, nós podemos simplesmente extender a classe Duck
e setar de alguma forma qual o comportamento que será usado. Aqui estamos setando através do construtor, mas poderia ser de outra forma também.
Como deixar os comportamentos dinâmicos em tempo de execução?
Um dos propósitos do Strategy é ter a flexibilidade da implementação ser trocada em tempo de execução, e para isso, na nossa classe Duck
, devemos adicionar dois métodos setter: setFlyBehavior()
e setQuackBehavior()
.
public abstract class Duck {
// ... Outros atributos e métodos
public void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}
public void setQuackBehavior(QuackBehavior quackBehavior) {
this.quackBehavior = quackBehavior;
}
}
Hora de testarmos nosso "Jogo de patos"
public static void main(String[] args) {
Duck mallard = new MallardDuck();
mallard.performQuack(); // Quack
mallard.performFly(); // FlyWithWings (voa com asas)
mallard.setFlyBehavior(new FlyNoWay());
mallard.performFly(); // FlyNoWay (não consegue voar)
}
Percebem que agora nossa aplicação ficou muito mais flexível e muito mais fácil de manter?
Caso a gente precise de um novo tipo de vôo, basta criarmos uma nova classe que implementa a interface FlyBehavior. Mesma coisa com um novo tipo de quack. O que terá que mudar será o código cliente, que deve conhecer os novos comportamentos para poder escolher o necessário.
Quando usar Strategy?
A escolha de utilizar o padrão de projeto Strategy é especialmente benéfica em diversas situações no desenvolvimento de software. Aqui estão alguns cenários em que o Strategy brilha e pode ser a solução ideal:
Comportamentos Variáveis: Utilize o Strategy quando você tem algoritmos ou comportamentos que podem variar e precisam ser escolhidos dinamicamente em tempo de execução. Isso proporciona uma flexibilidade significativa, permitindo que você adapte o comportamento do sistema sem modificar diretamente suas classes.
Evitar Herança Excessiva: Se você perceber que está criando uma hierarquia de classes com muitas subclasses apenas para fornecer diferentes implementações de um método, o Strategy oferece uma alternativa mais flexível e fácil de manter. Evite a armadilha da herança excessiva e opte pela composição.
Reutilização de Comportamentos: Quando você deseja reutilizar comportamentos específicos em diferentes partes do sistema, o Strategy permite encapsular esses comportamentos em classes independentes, promovendo a reutilização de código.
Manutenção e Extensibilidade: Use o Strategy para criar sistemas mais fáceis de manter e estender. A capacidade de adicionar novos comportamentos sem alterar o código existente é uma grande vantagem em projetos que exigem evolução contínua.
Testabilidade: O Strategy facilita a realização de testes unitários, pois você pode testar cada estratégia separadamente. Isso melhora a testabilidade do código e ajuda a garantir a robustez do sistema.
Strategy X Programação Funcional
Atualmente, diversas linguagens suportam programação funcional, e por conta da flexibilidade desse paradigma, em vários casos onde normalmente seria usado Strategy, pode ser usada a programação funcional no lugar, já que ela tem um efeito semelhante e elimina a necessidade de criar classes adicionais.
No nosso exemplo dos patos, ao invés de criarmos várias implementações de comportamentos, nós poderíamos passar o comportamento como parâmetro em uma interface funcional do Java.
public abstract class Duck {
protected Supplier<String> flyBehavior;
protected Supplier<String> quackBehavior;
public void performFly() {
System.out.println(flyBehavior.get());
}
public void performQuack() {
System.out.println(quackBehavior.get());
}
public void setFlyBehavior(Supplier<String> flyBehavior) {
this.flyBehavior = flyBehavior;
}
public void setQuackBehavior(Supplier<String> quackBehavior) {
this.quackBehavior = quackBehavior;
}
}
public class MallardDuck extends Duck {
public MallardDuck() {
quackBehavior = () -> "Quack";
flyBehavior = () -> "FlyWithWings (voa com asas)";
}
public void display() {
System.out.println("MallardDuck");
}
}
public static void main(String[] args) {
Duck mallard = new MallardDuck();
mallard.performQuack(); // Quack
mallard.performFly(); // FlyWithWings (voa com asas)
mallard.setFlyBehavior(() -> "FlyNoWay (não consegue voar)");
mallard.performFly(); /// FlyNoWay (não consegue voar)
}
A escolha entre usar o strategy ou programação funcional depende dos requisitos do seus sistema. Se são poucos comportamentos simples e raramente eles mudam, usar programação funcional é uma possibilidade. Agora se são comportamentos mais complexos, ou que podem ser reutilizados em outros locais, o strategy pode se adequar melhor.
É isso pessoal, espero que tenham aprendido algo de novo :) até o próximo pattern! (spoiler: Iremos falar sobre Observer)
Github com o código-fonte
Toda implementação de código você encontra no meu GitHub, lá você pode ver e testar o código por conta própria.
Referências
Deixo aqui algumas referências que usei para escrever esse post e também outros sites muito bons que explicam sobre os Design Patterns:
Top comments (0)