DEV Community

Leonardo Marques
Leonardo Marques

Posted on

Strategy: Uma perspectiva Head First

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();
}
Enter fullscreen mode Exit fullscreen mode

Também precisamos dos patos em si, então vamos criar as classes MallardDuck e RedheadDuck que herdam de Duck.

Diagrama de classe dos patos

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().

Diagrama de classes com 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.

Pato de Borracha voador?

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.

Diagrama Interface FlyBehavior

Interface QuackBehavior

Interface que será usada para definir o tipo de "quack" do pato, para cada tipo de quack, uma classe nova será criada.

Diagrama Interface QuackBehavior

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:

  1. Adicionar dois atributos (do tipo interface) que armazenam ambos comportamentos.
  2. Trocar métodos quack() e fly() para novos que apenas chamam o quack() e fly() dos comportamentos.

Diagrama de classe Duck após strategy

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

Classes após Strategy

public class FlyWithWings implements FlyBehavior {
   @Override
   public void fly() {
      System.out.println("FlyWithWings (voa com asas)");
   }
}
Enter fullscreen mode Exit fullscreen mode
public class FlyNoWay implements FlyBehavior {
   @Override
   public void fly() {
      System.out.println("FlyNoWay (não consegue voar)");
   }
}
Enter fullscreen mode Exit fullscreen mode
public class Quack implements QuackBehavior {
   @Override
   public void quack() {
      System.out.println("Quack");
   }
}
Enter fullscreen mode Exit fullscreen mode
public class MallardDuck extends Duck {
    public MallardDuck() {
        quackBehavior = new Quack();
        flyBehavior = new FlyWithWings();
    }

    public void display() {
        System.out.println("MallardDuck");
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)