CodeGym /Blogue Java /Random-PT /Padrão de Projeto de Estratégia
John Squirrels
Nível 41
San Francisco

Padrão de Projeto de Estratégia

Publicado no grupo Random-PT
Oi! Na lição de hoje, falaremos sobre o padrão Strategy. Nas lições anteriores, já nos familiarizamos brevemente com o conceito de herança. Caso você tenha esquecido, vou lembrá-lo de que esse termo se refere a uma solução padrão para uma tarefa comum de programação. No CodeGym, costumamos dizer que você pode pesquisar no Google a resposta para quase todas as perguntas. Isso porque sua tarefa, seja ela qual for, provavelmente já foi resolvida com sucesso por outra pessoa. Os padrões são soluções testadas e comprovadas para as tarefas mais comuns ou métodos para resolver situações problemáticas. São como "rodas" que você não precisa reinventar por conta própria, mas precisa saber como e quando usá-las :) Outro propósito para os padrões é promover uma arquitetura uniforme. Ler o código de outra pessoa não é tarefa fácil! Todo mundo escreve código diferente, porque a mesma tarefa pode ser resolvida de várias maneiras. Mas o uso de padrões ajuda diferentes programadores a entender a lógica de programação sem se aprofundar em cada linha de código (mesmo ao vê-la pela primeira vez!) Hoje veremos um dos padrões de design mais comuns chamado "Estratégia". Padrão de design: Estratégia - 2Imagine que estamos escrevendo um programa que funcionará ativamente com objetos de transporte. Realmente não importa o que exatamente nosso programa faz. Criamos uma hierarquia de classes com uma classe pai Conveyance e três classes filhas: Sedan , Truck e F1Car .

public class Conveyance {

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {

       System.out.println("Braking!");
   }
}

public class Sedan extends Conveyance {
}

public class Truck extends Conveyance {
}

public class F1Car extends Conveyance {
}
Todas as três classes filhas herdam dois métodos padrão do pai: go() e stop() . Nosso programa é muito simples: nossos carros só podem avançar e frear. Continuando nosso trabalho, decidimos dar aos carros um novo método: fill() (que significa "encha o tanque de gasolina"). Nós o adicionamos à classe pai Conveyance :

public class Conveyance {

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {

       System.out.println("Braking!");
   }
  
   public void fill() {
       System.out.println("Refueling!");
   }
}
Os problemas podem realmente surgir em uma situação tão simples? Na verdade, eles já têm... Padrão de design: Estratégia - 3

public class Stroller extends Conveyance {

   public void fill() {
      
       // Hmm... This is a stroller for children. It doesn't need to be refueled :/
   }
}
Nosso programa agora tem um meio de transporte (um carrinho de bebê) que não se encaixa muito bem no conceito geral. Pode ter pedais ou ser controlado por rádio, mas uma coisa é certa - não terá onde colocar gasolina. Nossa hierarquia de classes fez com que métodos comuns fossem herdados por classes que não precisam deles. O que devemos fazer nesta situação? Bem, poderíamos sobrescrever o método fill() na classe Stroller para que nada aconteça quando você tentar reabastecer o carrinho:

public class Stroller extends Conveyance {

   @Override
   public void fill() {
       System.out.println("A stroller cannot be refueled!");
   }
}
Mas isso dificilmente pode ser chamado de solução bem-sucedida, se não por outro motivo, a não ser código duplicado. Por exemplo, a maioria das classes usará o método da classe pai, mas o restante será forçado a substituí-lo. Se tivermos 15 classes e precisarmos substituir o comportamento em 5-6 delas, a duplicação de código se tornará bastante extensa. Talvez as interfaces possam nos ajudar? Por exemplo, assim:

public interface Fillable {
  
   public void fill();
}
Vamos criar uma interface Fillable com um método fill() . Então, os meios de transporte que precisam ser reabastecidos implementarão essa interface, enquanto outros meios de transporte (por exemplo, nosso carrinho de bebê) não. Mas esta opção não nos convém. No futuro, nossa hierarquia de classes pode crescer muito (imagine quantos tipos diferentes de meios de transporte existem no mundo). Abandonamos a versão anterior envolvendo herança, porque não queremos sobrescrever o método fill()método muitas e muitas vezes. Agora temos que implementá-lo em todas as aulas! E se tivermos 50? E se mudanças frequentes forem feitas em nosso programa (e isso quase sempre é verdade para programas reais!), Teríamos que correr por todas as 50 classes e alterar manualmente o comportamento de cada uma delas. Então, o que, no final, devemos fazer nessa situação? Para resolver nosso problema, vamos escolher uma maneira diferente. Ou seja, vamos separar o comportamento da nossa classe da própria classe. O que isso significa? Como você sabe, todo objeto tem estado (um conjunto de dados) e comportamento (um conjunto de métodos). O comportamento da nossa classe de transporte consiste em três métodos: go() , stop() e fill() . Os dois primeiros métodos são bons exatamente como são. Mas vamos mover o terceiro método para fora doClasse de transporte . Isso separará o comportamento da classe (mais precisamente, separará apenas parte do comportamento, pois os dois primeiros métodos permanecerão onde estão). Então, onde devemos colocar nosso método fill() ? Nada vem à mente :/ Parece que está exatamente onde deveria estar. Vamos movê-lo para uma interface separada: FillStrategy !

public interface FillStrategy {

   public void fill();
}
Por que precisamos dessa interface? É tudo simples. Agora podemos criar várias classes que implementam essa interface:

public class HybridFillStrategy implements FillStrategy {
  
   @Override
   public void fill() {
       System.out.println("Refuel with gas or electricity — your choice!");
   }
}

public class F1PitstopStrategy implements FillStrategy {
  
   @Override
   public void fill() {
       System.out.println("Refuel with gas only after all other pit stop procedures are complete!");
   }
}

public class StandardFillStrategy implements FillStrategy {
   @Override
   public void fill() {
       System.out.println("Just refuel with gas!");
   }
}
Criamos três estratégias comportamentais: uma para carros comuns, uma para híbridos e outra para carros de Fórmula 1. Cada estratégia implementa um algoritmo de reabastecimento diferente. Em nosso caso, simplesmente exibimos uma string no console, mas cada método pode conter alguma lógica complexa. O que faremos a seguir?

public class Conveyance {

   FillStrategy fillStrategy;

   public void fill() {
       fillStrategy.fill();
   }

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {
       System.out.println("Braking!");
   }
  
}
Usamos nossa interface FillStrategy como um campo na classe pai Conveyance . Observe que não estamos indicando uma implementação específica — estamos usando uma interface. As classes de carros precisarão de implementações específicas da interface FillStrategy :

public class F1Car extends Conveyance {

   public F1Car() {
       this.fillStrategy = new F1PitstopStrategy();
   }
}

public class HybridCar extends Conveyance {

   public HybridCar() {
       this.fillStrategy = new HybridFillStrategy();
   }
}

public class Sedan extends Conveyance {

   public Sedan() {
       this.fillStrategy = new StandardFillStrategy();
   }
}

Vamos ver o que temos!

public class Main {

   public static void main(String[] args) {

       Conveyance sedan = new Sedan();
       Conveyance hybrid = new HybridCar();
       Conveyance f1car = new F1Car();

       sedan.fill();
       hybrid.fill();
       f1car.fill();
   }
}
Saída do console:

Just refuel with gas! 
Refuel with gas or electricity — your choice! 
Refuel with gas only after all other pit stop procedures are complete!
Ótimo! O processo de reabastecimento funciona como deveria! A propósito, nada nos impede de usar a estratégia como parâmetro no construtor! Por exemplo, assim:

public class Conveyance {

   private FillStrategy fillStrategy;

   public Conveyance(FillStrategy fillStrategy) {
       this.fillStrategy = fillStrategy;
   }

   public void fill() {
       this.fillStrategy.fill();
   }

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {
       System.out.println("Braking!");
   }
}

public class Sedan extends Conveyance {

   public Sedan() {
       super(new StandardFillStrategy());
   }
}



public class HybridCar extends Conveyance {

   public HybridCar() {
       super(new HybridFillStrategy());
   }
}

public class F1Car extends Conveyance {

   public F1Car() {
       super(new F1PitstopStrategy());
   }
}
Vamos executar nosso método main() (que permanece inalterado). Obtemos o mesmo resultado! Saída do console:

Just refuel with gas! 
Refuel with gas or electricity — your choice! 
Refuel with gas only after all other pit stop procedures are complete!
O padrão de design de estratégia define uma família de algoritmos, encapsula cada um deles e garante que sejam intercambiáveis. Ele permite que você modifique os algoritmos independentemente de como eles são usados ​​pelo cliente (essa definição, tirada do livro "Head First Design Patterns", me parece excelente). Padrão de design: Estratégia - 4Já especificamos a família de algoritmos nos quais estamos interessados ​​(maneiras de reabastecer carros) em interfaces separadas com diferentes implementações. Nós os separamos do próprio carro. Agora, se precisarmos fazer alterações em um algoritmo de reabastecimento específico, isso não afetará nossas classes de carros de forma alguma. E para alcançar a intercambialidade, só precisamos adicionar um único método setter à nossa classe Conveyance :

public class Conveyance {

   FillStrategy fillStrategy;

   public void fill() {
       fillStrategy.fill();
   }

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {
       System.out.println("Braking!");
   }

   public void setFillStrategy(FillStrategy fillStrategy) {
       this.fillStrategy = fillStrategy;
   }
}
Agora podemos mudar as estratégias na hora:

public class Main {

   public static void main(String[] args) {

       Stroller stroller= new Stroller();
       stroller.setFillStrategy(new StandardFillStrategy());

       stroller.fill();
   }
}
Se os carrinhos de bebê de repente começarem a funcionar com gasolina, nosso programa estará pronto para lidar com esse cenário :) E é isso! Você aprendeu mais um padrão de projeto que, sem dúvida, será essencial e útil ao trabalhar em projetos reais :) Até a próxima!
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION