Padrão de Projeto: Strategy (Estratégia)
(12089)
CategoriasAndroid, Design, Protótipo
AutorVinÃcius Thiengo
VÃdeo aulas186
Tempo15 horas
ExercÃciosSim
CertificadoSim
CategoriaEngenharia de Software
Autor(es)Vaughn Vernon
EditoraAlta Books
Edição1ª
Ano2024
Páginas160
Tudo bem?
Neste artigo vamos falar sobre o padrão de projeto Strategy.
Padrão simples de utilizar e robusto quando as opções herança e implementação de Interface na hierarquia atual de classes não ajudam o suficiente na resolução do problema.
Antes de prosseguir, não esqueça de se inscrever na 📫 lista de e-mails do Blog para receber em primeira mão todos os conteúdos exclusivos sobre desenvolvimento e codificação limpa.
Abaixo os tópicos presentes neste artigo:
Apresentação
Vamos começar com a definição do padrão:
Defini famílias de algoritmos implementados por uma hierarquia de classes onde os objetos dessas classes são utilizados em objetos de classes clientes por meio de composição, permitindo assim a mudança de comportamento dos objetos clientes pela intercambiação de objetos de estratégia.
Confuso? Somente pela definição provavelmente sim.
No exemplo deste artigo ficará mais claro o uso do padrão.
Tenha em mente que o polimorfismo por meio de herança nem sempre é a melhor escolha para termos um código mais flexível e passível a evolução.
Nesses casos o padrão Strategy é uma excelente opção de modelo de código a ser utilizado.
Com o padrão Strategy, como acontece com outros padrões de projeto, vamos estar implementando alguns princípios de orientação a objetos.
Caso ainda não tenha se deparado com esse tipo de questionamento aqui no blog, volto a fazê-lo:
Você sabe a importância dos princípios de orientação a objetos em projetos orientados e objetos?
São os princípios que precedem os padrões.
Todos os padrões aplicam ao menos um princípio, não confunda com: cada padrão ter ao menos um único princípio dele.
Na verdade os princípios de orientação a objetos são compartilhados entre todos os padrões.
No decorrer do conteúdo vamos estar apresentando os utilizados na aplicação do padrão Strategy.
Diagrama
Abaixo o diagrama do padrão proposto:
O Contexto representa as classes clientes das classes de Strategy.
As classes concretas de Strategy se responsabilizam por dar vida a família de classes proposta pelo padrão, no caso, definindo os comportamentos, métodos.
O método interfaceContexto() está representando uma série de métodos de trabalho com instâncias Strategy, métodos getter e setter, por exemplo.
Strategy pode ser uma classe abstrata ou uma Interface. Se implementada como uma classe abstrata muito provavelmente é porque algum dos métodos têm implementação comum em duas ou mais classes abaixo, na hierarquia Strategy.
Essa alternância em utilizar classe abstrata ao invés de Interface é algo que pode ocorrer além de algumas outras modificações no modelo de implementação indicado pelo padrão, tendo em mente que cada padrão de projeto têm seu modelo de implementação comum, mas que é, esse modelo, completamente passivo de modificação para se adaptar a nossos projetos de software.
Provavelmente ainda não está claro o uso do padrão Strategy, também não ficaria a mim, somente com textos e diagramas. Logo vamos prosseguir com o código de exemplo.
Código de exemplo
Neste artigo vamos utilizar parte de um game de felinos, onde há como subclasses:
- Felinos reais;
- e Felinos de brinquedo.
Vamos primeiro a uma parte do diagrama de classes atual desse jogo:
Vamos utilizar somente uma parte do game?
Sim. É importante que saiba que na verdade existem dezenas de subclasses de Felino no projeto, porém aqui, para poupar espaço, vamos trabalhar somente com as apresentadas acima.
Vamos primeiro ao código atual da superclasse Felino:
abstract public class Felino {
abstract public void display();
abstract public void rugir();
}
Agora da subclasse Leao:
public class Leao extends Felino {
@Override
public void display(){
System.out.println( "Leão a vista" );
}
@Override
public void rugir(){
System.out.println( "Urghhhhh!" );
}
}
Então a subclasse Leopardo:
public class Leopardo extends Felino {
@Override
public void display(){
System.out.println( "Leopardo a vista" );
}
@Override
public void rugir(){
System.out.println( "Arghh!" );
}
}
E por fim a subclasse TigreToy (que nome é esse?!):
public class TigreToy extends Felino {
@Override
public void display(){
System.out.println( "Tigre de borracha na banheira" );
}
@Override
public void rugir(){
System.out.println( "Uen! Uen!" );
}
}
Sabemos que algo que é permanente em qualquer software é a evolução dele, alias essa é a parte mais crítica e demorada do projeto, pois as vezes os programadores são outros, não os que construíram o software.
Nesse contexto do game de felinos, nós recebemos um schedule (checklist de novas funcionalidades) de evolução de software.
Temos que adicionar dois outros comportamentos aos felinos, ao menos aos felinos que têm, no mundo real, os comportamentos solicitados.
Os comportamentos são: correr e pular.
No big deal!
Somente devemos adicionar esses métodos como abstratos (ou não, em caso de implementações comuns) na superclasse Felino.
Será?
Ok, aparentemente essa é uma boa opção. Mesmo que algumas subclasses de Felino não possam correr ou pular.
Nessas subclasses que não têm esses comportamentos no mundo real, podemos ter implementações próprias desses métodos que vão “fazer nada”, ou seja, um print simples ou o conteúdo vazio mesmo.
Não há uma solução ainda mais sofisticada?
Sim, há. Podemos criar Interfaces para que esses comportamentos somente entrem em subclasses que realmente precisem deles.
As Interfaces poderiam ser CorrerImpl e PularImpl.
Dessa forma, ao menos Leao e Leopardo as implementariam e assim teríamos uma classe base com somente comportamentos e atributos comuns as subclasses, algo que é desejável em um código orientado a objetos, limpo.
Sabe o por quê do sufixo "Impl" nos nomes de Interface?
Quando não se tem um padrão de projeto sendo aplicado a ponto de utilizar o nome do padrão como parte do nome das entidades que aplicam esse padrão (classes e Interfaces) ao software, digo, o nome do padrão como sufixo ou prefixo dos nomes dessas entidades.
Nesse caso, quando se utilizando Interfaces, uma maneira de melhorar a leitura de código é colocando como sufixo algo que indica aos programadores que aquele tipo sendo visualizado na verdade é uma Interface.
Quando os programadores do projeto entendem isso como uma convenção de desenvolvimento, a leitura do código fica ainda mais eficiente.
Vamos olhar mais de perto os possíveis problemas das duas soluções apresentadas até aqui:
- Trabalhando com Interfaces nós perderíamos parte da flexibilidade do código. Pois em trechos que os métodos correr() e pular() forem utilizados será necessário a identificação do tipo do objeto, para que não sejam invocados esses métodos em instâncias que não os tenham;
- Ambas as opções citadas ferem o princípio de orientação a objetos "Dê prioridade a composição ao invés da herança." Somente ferem ele, pois a composição é uma solução possível nesse trecho do game, mesmo que ainda não discutida (spoiler: é a versão com Strategy);
- Nesse projeto qualquer outra nova funcionalidade implica em refatorar o projeto inteiro com novos métodos na superclasse Felino (incluindo algumas subclasses com implementações próprias) ou novas Interfaces a serem implementadas.
Resumo dos problemas: as soluções indicadas são possíveis, porém tiram a flexibilidade da evolução do software.
Nesse caso devemos tentar uma solução mais viável, caso exista alguma. É ai que entra o padrão de projeto Strategy.
Vamos criar famílias de classes para os comportamentos correr e pular. Começando com o comportamento correr.
Vamos criar uma Interface CorrerStrategy:
public interface CorrerStrategy {
public void correr();
}
Essa Interface é equivalente a entidade Strategy do diagrama do padrão proposto aqui.
A partir dela poderemos criar várias classes concretas referentes ao comportamento de correr. Classes que deverão implementar o método chave, correr().
Um leão não corre como um leopardo, ele é mais lento e pesado, além disso é um animal que dorme a maior parte do tempo de um dia.
Com isso temos indícios de que ele somente corre quando é o ponto chave da caça, bem próximo da presa.
Então o correr do leão pode ser referenciado pela classe CorrerCurtaDist:
public class CorrerCurtaDist implements CorrerStrategy {
@Override
public void correr(){
System.out.println( "Correr curta distância, porém com o objetivo bem próximo." );
}
}
Acima nossa primeira classe estratégia concreta.
Assim já temos ao menos uma família de algoritmos pronta para ser utilizada. Apesar de não termos essa família dentro de códigos clientes ela já representa o padrão Strategy.
Agora o leopardo.
Este é bem mais rápido que o leão, porém não tão forte e pesado, com isso ele pode correr longas distâncias sem muitos problemas, até porque algumas presas conseguem lutar, se soltar e correrem novamente.
A classe CorrerLongaDist representa melhor o comportamento de correr do leopardo:
public class CorrerLongaDist implements CorrerStrategy {
@Override
public void correr(){
System.out.println( "Correr longa distância, caso necessário." );
}
}
E para a subclasse de brinquedo, digo, TigreToy. O que fazer? Um brinquedo não corre.
Para esse tipo de situação vamos criar uma subclasse que "faz nada":
public class CorrerToy implements CorrerStrategy {
@Override
public void correr(){
System.out.println( "Sou um brinquedo, não corro." );
}
}
CorrerToy poderá ser compartilhado por todos os felinos de brinquedo.
Optamos aqui por colocar um print indicando que não é possível correr. Mas deixar o método vazio é uma escolha completamente válida.
Ao invés de prosseguir criando uma outra família de classes, digo, para o comportamento pular, vamos primeiro colocar o comportamento correr nas subclasses de Felino.
Vamos colocar como variável de instância, na classe Felino, uma variável do tipo CorrerStrategy e também um método set para essa variável:
abstract public class Felino {
protected CorrerStrategy correrStrategy;
abstract public void display();
abstract public void rugir();
public void setCorrerStrategy( CorrerStrategy correrStrategy ){
this.correrStrategy = correrStrategy;
}
public void performarCorrer(){
correrStrategy.correr();
}
}
Note que também adicionamos um método perfomarCorrer(). Com isso podemos somente delegar a tarefa a instância do comportamento que foi vinculado ao felino.
Lembrando que a delegação é necessária, pois os comportamentos não estão sendo vinculados via herança as classes de contexto, aqui, as subclasses de Felino, e sim via composição por meio de famílias de algoritmos.
Notou algo? Estamos utilizando o tipo CorrerStrategy e não os tipos especializados dessa Interface.
Essa escolha na verdade é a utilização de um princípio de orientação a objetos que diz: "Programe para interface e não para implementação."
O que? Então sempre devemos optar por Interfaces ao invés de classes?
Não.
Na verdade esse "interface" é referente a supertipo (classe, classe abstrata ou Interface estrutura da linguagem), ou seja, indica que devemos utilizar um supertipo que permita ser possível trabalhar no código cliente sem necessidade de identificação das classes especialistas desse supertipo.
No caso acima, as classes especialistas de CorrerStrategy seriam:
- CorrerCurtaDist
- e CorrerLongaDist.
Isso somente é possível se os métodos públicos utilizados nos códigos clientes forem exatamente os mesmos métodos públicos do supertipo.
Em nosso caso a interface CorrerStrategy é um supertipo válido, pois ela tem declarado o único método público utilizado no código cliente, correr().
Agora podemos atualizar as três subclasses de Felino, começando por Leao:
public class Leao extends Felino {
public Leao(){
correrStrategy = new CorrerCurtaDist();
}
...
}
Note que colocamos um construtor em Leao, onde diretamente instanciamos o tipo correr utilizado por instâncias dessa classe.
Poderíamos utilizar o método setCorrerStrategy() para somente instanciarmos um CorrerStrategy quando fosse necessário, isso se chama injeção de dependência, fica a seu critério trabalhar dessa forma ou não.
Curiosidade
Injeção de Dependência é um padrão de software muito útil, pois permite a criação de objetos somente quando esses são necessários ao algoritmo, conseguindo assim uma melhor performance dos recursos do sistema onde o software está sendo executado.
Você provavelmente deve ter pensado quando viu o método setCorrerStrategy():
O que esse método set está fazendo ai se cada Felino terá somente a maneira única dele de expressar o comportamento?
Na verdade isso é parte de um game e é bem provável que de acordo com o estágio do jogador ele possa utilizar outros modos de correr com o mesmo felino. como informado, isso é um game.
Lembrando também que essa restrição de "não mudança do tipo de comportamento" nunca foi informada como parte do projeto.
Com esse modelo de código, comportamento sendo injetado via composição, a alternância no modo de correr pode ser realizada somente com um novo objeto de CorrerStrategy sendo injetado via setCorrerStrategy(), com isso o código é dinâmico em tempo de execução, uma das principais vantagens do padrão Strategy.
Com as duas primeiras soluções citadas, utilizando herança e implementação direta de Interface, não seria possível essa mudança de comportamento de forma dinâmica.
Note também que a implementação do comportamento via composição permite que o projeto seja mais flexível quanto a evolução, pois novos modos de correr, por exemplo, somente implica em novas subclasses de CorrerStrategy, nada é alterado, apenas adicionado.
Recordando que "adicionar" é algo bom, "alterar" é um provável indício de código que ainda precisa ser melhorado.
Com isso o princípio de orientação a objetos que era violado agora é aplicado corretamente. Qual princípio? O seguinte: "Dê prioridade a composição ao invés da herança."
Vamos às atualizações em Leopardo:
public class Leopardo extends Felino {
public Leopardo(){
correrStrategy = new CorrerLongaDist();
}
...
}
E então em TigreToy:
public class TigreToy extends Felino {
public TigreToy(){
correrStrategy = new CorrerToy();
}
...
}
Dessa forma podemos ter o seguinte código cliente das subclasses de Felino:
public class FelinoCliente {
public static void main( String args[] ){
Felino leao = new Leao();
Felino leopardo = new Leopardo();
Felino tigreToy = new TigreToy();
leao.performarCorrer(); /* Correr curta distância, porém com o objetivo bem próximo. */
leopardo.performarCorrer(); /* Correr longa distância, caso necessário. */
tigreToy.performarCorrer(); /* Sou um brinquedo, não corro. */
/* LEOPARDO DE BARRIGA CHEIA, CORRENDO COMO LEÃO PARA PEGAR A JANTA! */
leopardo.setCorrerStrategy( new CorrerCurtaDist() );
leopardo.performarCorrer(); /* Correr curta distância, porém com o objetivo bem próximo. */
}
}
Bom, com isso temos nosso Strategy implementado por completo.
Recapitulando:
- O código que variava, referente a correr (daqui a pouco vamos também implementar o referente a pular), nós encapsulamos em uma família de classes, permitindo assim a reutilização do código por outras classes, não somente as de Felino;
- Com o Strategy damos preferência a composição ao invés da herança, além de trabalharmos com interface comum ao invés de implementação. Com isso conseguimos um código que permite uma evolução de projeto mais eficiente, pois o código cliente das famílias de classes de estratégia não precisa ser alterado e temos ainda as vantagens do uso do polimorfismo.
Agora para finalizarmos o projeto vamos a implementação do comportamento pular.
Começando com a Interface PularStrategy:
public interface PularStrategy {
public void pular();
}
As explicações para as entidades referentes ao comportamento de pular são exatamente como as de correr, logo vamos seguir apenas implementando.
Depois da entidade Strategy vamos as implementações de apenas duas subclasses estratégias concretas, pois os pulos do leopardo e leão vamos tratar como iguais.
Segue classe PuloAlto, referente ao felinos reais:
public class PuloAlto implements PularStrategy {
@Override
public void pular(){
System.out.println( "Pulo alto e objetivo." );
}
}
E então PuloToy, para brinquedos:
public class PuloToy implements PularStrategy {
@Override
public void pular(){
System.out.println( "Sou um brinquedo, não pulo." );
}
}
Com isso podemos ir a classe Felino e atualizar com os códigos para nossa nova família de classes Strategy, PularStrategy:
abstract public class Felino {
...
protected PularStrategy pularStrategy;
...
public void setPularStrategy(PularStrategy pularStrategy){
this.pularStrategy = pularStrategy;
}
public void performarPular(){
pularStrategy.pular();
}
}
Assim, seguramente podemos iniciar a variável pularStrategy nas subclasses de Felino.
Começando por Leao:
public class Leao extends Felino {
public Leao(){
correrStrategy = new CorrerCurtaDist();
pularStrategy = new PuloAlto();
}
...
}
Agora Leopardo:
public class Leopardo extends Felino {
public Leopardo(){
correrStrategy = new CorrerLongaDist();
pularStrategy = new PuloAlto();
}
...
}
E então TigreToy:
public class TigreToy extends Felino {
public TigreToy(){
correrStrategy = new CorrerToy();
pularStrategy = new PuloToy();
}
...
}
Você provavelmente notou que para Leao e Leopardo o código de inicialização para pularStrategy é exatamente o mesmo.
Uma possível melhoria é colocar esse trecho de código em um novo construtor em Felino.
Mas é aquilo que já informamos, são dezenas de classes abaixo na hierarquia de Felino, o código repetido mais vezes deve ir para a implementação comum dessa superclasse, logo vamos manter o código como está, nosso objetivo aqui é apenas aplicar o Strategy.
O código cliente utilizando o comportamento de pulo é um pouco mais simples que o anterior, não foi necessária a amostra da troca de comportamento, como fizemos com a variável leopardo anteriormente, pois o processo é o mesmo.
Segue código cliente atualizado:
public class FelinoCliente {
public static void main( String args[] ){
Felino leao = new Leao();
Felino leopardo = new Leopardo();
Felino tigreToy = new TigreToy();
leao.performarPular(); /* Pulo alto e objetivo. */
leopardo.performarPular(); /* Pulo alto e objetivo. */
tigreToy.performarPular(); /* Sou um brinquedo, não pulo. */
}
}
Assim finalizamos a apresentação do padrão.
Note que apesar de o Strategy adicionar funcionalidades por meio de sua própria família de classes ele não é um substituto do padrão Decorator.
Eles, apesar de terem definições um pouco similares, têm contextos de implementação diferentes.
Vamos então o novo diagrama do projeto do game de felinos:
Algo a mais que se deve notar é que mesmo utilizando Interfaces como sendo as entidades Strategy do game de felinos poderíamos sim ter utilizado classe abstrata com até mesmo implementações comuns a algumas subclasses concretas de Strategy.
Pontos negativos
- O número de classes no projeto pode crescer consideravelmente se houverem muitas funcionalidades que não podem ser implementadas por classes de uma mesma família de classes Strategy;
- Pode complicar o projeto ainda mais caso uma solução com herança seja mais simples de implementar e manter.
Pontos positivos
- Como qualquer outro padrão, coloca uma linguagem universal ao menos em parte do projeto, com isso permitindo que a leitura do mesmo seja mais eficiente;
- A reutilização de código é evidente, sendo que as famílias de algoritmos podem ser utilizadas por classes clientes de diferentes contextos, por exemplo, classes de contexto "humanos" poderiam utilizar as classes estratégia de correr e pular;
- Permite uma evolução de projeto mais eficiente, pois o foco é na composição, evitando que classes e subclasses clientes sejam alteradas caso novas funcionalidades sejam adicionadas ou atualizadas;
- A possibilidade de mudança de comportamento em tempo de execução da maior dinamismo ao projeto, permitindo assim mais opções de funcionalidades.
Conclusão
Com o padrão Strategy será possível reutilizar o código por todo o projeto caso necessário.
Além de permitir que as subclasses de uma família de classes clientes de Strategy mantenham-se sendo utilizadas sem necessidade de verificação de tipo.
Isso devido ao polimorfismo.
Porém caso o dinamismo de código não seja um requisito de projeto, pode ser que o trabalho com herança seja a melhor opção, principalmente pela não necessidade de novas classes.
Então é isso.
Por fim, não deixe de se inscrever na 📩 lista de e-mails do Blog para receber os conteúdos de desenvolvimento e codificação limpa exclusivos... e em primeira mão.
Abraço.
Fontes
Use a Cabeça! Padrões de Projetos.
Comentários Facebook