O padrão de estratégia explicado usando Java

Abdul Kadir Blocked Desbloquear Seguir Seguindo 7 de janeiro

Neste post, vou falar sobre um dos padrões de design populares – o padrão de estratégia. Se você ainda não está ciente, os padrões de design são um conjunto de princípios de programação orientados a objetos criados por nomes notáveis na indústria de software, geralmente chamado de Gang of Four (GoF) . Esses padrões de Design causaram um enorme impacto no ecossistema de software e são usados até hoje para resolver problemas comuns enfrentados na Programação Orientada a Objetos.

Vamos definir formalmente o padrão de estratégia:

O padrão Strategy define uma família de algoritmos, encapsula cada um deles e os torna intercambiáveis. A estratégia permite que o algoritmo varie independentemente dos clientes que o usam

Tudo bem com isso fora do caminho, vamos mergulhar em algum código para entender o que essas palavras REALMENTE significam. Vamos dar um exemplo com uma potencial armadilha e, em seguida, aplicar o padrão de estratégia para ver como ele supera o problema.

Eu vou estar mostrando como criar um programa simulador de cão dopado para aprender o padrão de estratégia. Veja como serão nossas classes: Uma superclasse de 'Cachorro' com comportamentos comuns e, em seguida, classes concretas de Cachorro criadas pela subclasse da classe Cachorro.

Aqui está o que o código parece

 classe abstrata pública Dog { 
display void abstrato público (); // cães diferentes têm visuais diferentes!
 public void eat () {} 
public void bark () {}
// Outros métodos semelhantes a cães
...
}

O método display () é feito abstrato, já que cães diferentes têm visuais diferentes. Todas as outras subclasses herdarão os comportamentos de comer e latir ou substituí-lo por sua própria implementação. Por enquanto, tudo bem!

Agora, e se você quisesse adicionar algum novo comportamento? Vamos dizer que você precisa de um cão robô legal que possa fazer todos os tipos de truques. Não é um problema, nós só precisamos adicionar um método performTricks () em nossa superclasse Dog e estamos prontos.

Mas espere um minuto … Um cachorro robô não deveria poder comer direito? Objetos inanimados não podem comer, é claro. Certo, como resolvemos esse problema então? Bem, podemos substituir o método eat () para não fazer nada e funciona muito bem!

 classe pública RobotDog estende o cão { 
@sobrepor
public void eat () {} // Não faça nada
 } 

Bem feito! Agora os cães-robô não podem comer, eles só podem latir ou fazer truques. E quanto aos cães de borracha? Eles não podem comer nem podem fazer truques. E cães de madeira não podem comer, latir ou fazer truques. Nem sempre podemos substituir métodos para não fazer nada, não é limpo e apenas parece hacky. Imagine fazer isso em um projeto cuja especificação de design continua mudando a cada poucos meses. O nosso é apenas um exemplo ingênuo, mas você tem a idéia. Então, precisamos encontrar uma maneira mais limpa de resolver esse problema.

A interface pode resolver nosso problema?

Como sobre interfaces? Vamos ver se eles podem resolver o nosso problema. Tudo bem, então criamos uma interface CanEat e CanBark:

 interface CanEat { 
public void eat ();
 } 
 interface CanBark { 
latido de vazio público ();
 } 

Nós removemos os métodos bark () e eat () da superclasse Dog e os adicionamos às respectivas interfaces. Assim, somente os cães que podem latir implementarão a interface do CanBark e os cães que comerão implementarão a interface CanEat. Agora, não mais se preocupar com cães herdando comportamento que eles não deveriam, nosso problema está resolvido … ou é?

O que acontece quando temos que fazer uma mudança no comportamento alimentar dos cães? Digamos que, a partir de agora, cada cão deve incluir uma certa quantidade de proteína em sua refeição. Agora você tem que modificar o método eat () de todas as subclasses de Dog. E se houver 50 dessas classes, oh, o horror!

Então, as interfaces só resolvem parcialmente o nosso problema de Cães fazendo apenas o que elas são capazes de fazer – mas elas criam outro problema completamente. As interfaces não têm nenhum código de implementação, portanto, há reutilização de código zero e potencial para muitos códigos duplicados. Como resolvemos isso, você pergunta? Padrão de estratégia vem para o resgate!

O padrão de estratégia

Então, vamos fazer isso passo a passo. Antes de prosseguir, deixe-me apresentar-lhe um princípio de design:

Identifique as partes do seu programa que variam e separe-as do que permanece o mesmo.

Na verdade, é muito simples – o princípio afirma separar e “encapsular” qualquer coisa que muda com frequência, de modo que todo o código que muda vive em um só lugar. Dessa forma, o código que muda não terá nenhum efeito sobre o resto do programa e a nossa aplicação é mais flexível e robusta.

No nosso caso, o comportamento 'bark' e 'eat' pode ser retirado da classe Dog e pode ser encapsulado em outro lugar. Sabemos que esses comportamentos variam entre os diferentes cães e eles devem ter sua própria turma separada.

Nós vamos criar dois conjuntos de classes além da classe Dog, um para definir o comportamento alimentar e outro para o comportamento de latir. Utilizaremos interfaces para representar o comportamento, como 'EatBehavior' e 'BarkBehavior', e a classe de comportamento concreto implementará essas interfaces. Então, a classe Dog não está mais implementando a interface. Estamos criando classes separadas cujo único trabalho é representar o comportamento específico!

É assim que a interface EatBehavior se parece

 interface EatBehavior { 
public void eat ();
}

E BarkBehavior

 interface BarkBehavior { 
latido de vazio público ();
}

Todas as classes que representam esses comportamentos implementarão a respectiva interface.

Classes concretas para BarkBehavior

 public class PlayfulBark implementa BarkBehavior { 
@sobrepor
public void bark () {
System.out.println ("Bark! Bark!");
}
}
 classe pública Growl implementa BarkBehavior { 
@sobrepor
public void bark () {
System.out.println ("Isso é um grunhido");
}
 classe pública MuteBark implementa BarkBehavior { 
@sobrepor
public void bark () {
System.out.println ("Este é um latido mudo");
}

Aulas concretas para o EatBehavior

 classe pública NormalDiet implementa EatBehavior { 
@sobrepor
public void eat () {
System.out.println ("Esta é uma dieta normal");
}
}
 classe pública ProteinDiet implementa EatBehavior { 
@sobrepor
public void eat () {
System.out.println ("Esta é uma dieta de proteína");
}
}

Agora, enquanto fazemos implementações concretas subclassificando a superclasse 'Dog', naturalmente queremos poder atribuir dinamicamente os comportamentos às instâncias dos cães. Afinal, era a inflexibilidade do código anterior que estava causando o problema. Podemos definir métodos setter na subclasse Dog que nos permitirá definir diferentes comportamentos em tempo de execução.

Isso nos leva a outro princípio de design:

Programe para uma interface e não uma implementação.

O que isto significa é que, em vez de usar as classes concretas, usamos variáveis que são supertipos dessas classes. Em outras palavras, usamos variáveis do tipo EatBehavior e BarkBehavior e atribuímos a essas variáveis objetos de classes que implementam esses comportamentos. Dessa forma, as classes Dog não precisam ter nenhuma informação sobre os tipos reais de objetos dessas variáveis!

Para tornar o conceito claro, aqui está um exemplo que diferencia as duas formas – Considere uma classe Animal abstrata que tenha duas implementações concretas, Dog e Cat.

A programação para uma implementação seria:

 Dog d = new Dog (); 
d.bark ();

Aqui está a programação de uma interface:

 Animal animal = novo cão (); 
animal.animalSound ();

Aqui, sabemos que o animal contém uma instância de um 'Cão', mas podemos usar essa referência polimorficamente em qualquer outro lugar em nosso código. Tudo o que nos importa é que a instância animal seja capaz de responder ao método animalSound () e o método apropriado, dependendo do objeto designado, seja chamado.

Isso foi muito para acontecer. Sem mais explicações, vamos ver como nossa superclasse 'Dog' se parece agora:

 classe abstrata pública Dog { 
EatBehavior eatBehavior;
BarkBehaviour barkBehavior;
 cão público () {} 
 public void doBark () { 
barkBehavior.bark ();
}
 public void doEat () { 
eatBehavior.eat ();
}
}

Preste muita atenção aos métodos desta classe. A classe Dog agora está 'delegando' a tarefa de comer e latir em vez de implementar por si só ou herdá-la (subclasse). No método doBark (), simplesmente chamamos o método bark () no objeto referenciado por barkBehavior. Agora, não nos importamos com o tipo real do objeto, apenas nos importamos se ele sabe latir!

Agora o momento da verdade, vamos criar um cachorro concreto!

 classe pública Labrador estende o cão { 
 Labrador público () { 
barkBehavior = novo PlayfulBark ();
eatBehavior = new NormalDiet ();
}
 public void display () { 
System.out.println ("Eu sou um labrador brincalhão");
}
...
}

O que está acontecendo no construtor da classe Labrador? estamos atribuindo as instâncias concretas ao supertipo (lembre-se de que os tipos de interface são herdados da superclasse Dog). Agora, quando chamamos doEat () na instância do Labrador, a responsabilidade é passada para a classe ProteinDiet e ela executa o método eat ().

O padrão de estratégia em ação

Tudo bem, vamos ver isso em ação. Chegou a hora de executar o nosso programa simulador Dog Dope!

 classe pública DogSimulatorApp { 
public static void main (String [] args) {
Dog lab = novo Labrador ();

lab.doEat (); // Prints "Esta é uma dieta normal"
lab.doBark (); // "Casca casca!"
}
}

Como podemos melhorar este programa? Adicionando flexibilidade! Vamos adicionar métodos setter na classe Dog para poder trocar comportamentos em tempo de execução. Vamos adicionar mais dois métodos à superclasse Dog:

 public void setEatBehavior (EatBehavior eb) { 
eatBehavior = eb;
}
 public void setBarkBehavior (BarkBehavior bb) { 
barkBehavior = bb;
}

Agora podemos modificar nosso programa e escolher o comportamento que quisermos em tempo de execução!

 classe pública DogSimulatorApp { 
public static void main (String [] args) {
Dog lab = novo Labrador ();

lab.doEat (); // Esta é uma dieta normal
lab.setEatBehavior (new ProteinDiet ());
lab.doEat (); // Esta é uma dieta de proteína
 lab.doBark (); // Latido! Latido! 
}
}

Vamos dar uma olhada no quadro geral:

Diagrama de Classes

Temos a superclasse Dog e a classe 'Labrador' que é uma subclasse de Dog. Em seguida, temos a família de algoritmos (Behaviors) “encapsulados” com seus respectivos tipos de comportamento.

Dê uma olhada na definição formal que dei no começo: os algoritmos nada mais são do que as interfaces de comportamento. Agora eles podem ser usados não apenas neste programa, mas outros programas também podem utilizá-lo. Observe os relacionamentos entre as classes no diagrama. Os relacionamentos IS-A e HAS-A podem ser inferidos do diagrama.

É isso aí! Espero que você tenha uma visão geral do padrão Strategy. O padrão de estratégia é extremamente útil quando você tem determinados comportamentos em seu aplicativo que mudam constantemente.

Isso nos leva ao final da implementação do Java. Muito obrigado por ficar comigo até agora! Se você estiver interessado em aprender sobre a versão Kotlin, fique atento para o próximo post. Eu falo sobre recursos de linguagem interessantes e como podemos reduzir todo o código acima em um único arquivo Kotlin 🙂