O PADRÃO STRATEGY
Ao codificar um programa, seguimos um conjunto de regras de negócio e, muitas vezes, o fluxo precisa ser dividido em vários caminhos. Quando o código cresce com várias regras diferentes, podemos recorrer aos padrões comportamentais para melhorar a flexibilidade e facilitar a manutenção.
Padrões comportamentais vão dividir as responsabilidades para resolver problemas de complexidade do código. O Strategy é um exemplo de padrão comportamental que busca isolar os vários caminhos que o algoritmo pode seguir, facilitando escolher um fluxo específico sem precisar se preocupar com os outros.
Exemplo de Uso
Na Listagem 5 temos a implementação do código que deve calcular os pontos que um passageiro ganhou em voos pela companhia aérea. As regras variam de acordo com a distância voada, o status do passageiro e o tipo de bilhete comprado.
Dado um passageiro, o algoritmo busca a lista de voos que ainda não foram computados e, para cada um deles aplica as regras de pontuação. Caso o bilhete seja de primeira classe e o passageiro tenha a categoria diamante, a distância total do voo será convertida em pontos. Caso o passageiro não seja diamante, ele receberá metade da distância em voos de primeira classe, um quarto em voos econômicos e um décimo em voos promocionais.
public int calcularPontuacao(Passageiro passageiro) {
int totalDePontos = 0;
List<Voo> voosPendentes = passageiro.getVoosComPontuacaoPendente();
for (Voo voo : voosPendentes) {
Bilhete bilhete = passageiro.getBilhete(voo.getNumero());
if (bilhete.isPrimeiraClasse() && passageiro.isDiamante()) {
totalDePontos += voo.getDistancia();
} else if (bilhete.isPrimeiraClasse()) {
totalDePontos += voo.getDistancia()/2;
} else if (bilhete.isClasseEconomica()) {
totalDePontos += voo.getDistancia()/4;
} else {
// bilhete promocional
totalDePontos += voo.getDistancia()/10;
}
}
passageiro.atualizarPontuacao(totalDePontos);
passageiro.marcarVoosComoProcessados();
return totalDePontos;
}
Separando os fluxos do algoritmo, conseguimos extrair 4 estratégias: 1) voo de um passageiro diamante na primeira classe, 2) voo na primeira classe, 3) voo na classe econômica e 4) voo promocional.
A interface comum das estratégias é bem simples e precisa apenas oferecer um método que retorna a pontuação do passageiro, dado a distância do voo. Uma Interface poderia ser implementada conforme a Listagem a seguir:
interface EstrategiaDePontuacao {
public int calcularPontuacao(int distanciaDeVoo);
}
A implementação das estratégias também é bem simples e pequena, pois as regras separadas são simples. Veja como ficaria o código na Listagem 7:
class EstrategiaPrimeiraClasseDiamante implements EstrategiaDePontuacao {
public int calcularPontuacao(int distanciaDeVoo){
return distanciaDeVoo;
}
}
class EstrategiaPrimeiraClasse implements EstrategiaDePontuacao {
public int calcularPontuacao(int distanciaDeVoo){
return distanciaDeVoo/2;
}
}
class EstrategiaClasseEconomica implements EstrategiaDePontuacao {
public int calcularPontuacao(int distanciaDeVoo){
return distanciaDeVoo/4;
}
}
class EstrategiaPromocional implements EstrategiaDePontuacao {
public int calcularPontuacao(int distanciaDeVoo){
return distanciaDeVoo/10;
}
}
Um ponto de atenção ao aplicar o padrão Strategy é encontrar onde deve ser decidido qual estratégia utilizar. Para determinar a regra de pontuação precisamos de informações de um passageiro e do bilhete. Vamos então criar um método na classe Bilhete que retorna qual a estratégia de pontuação deve ser utilizada. Veja a implementação na Listagem seguinte:
class Bilhete {
public EstrategiaDePontuacao getEstrategiaDePontuacao(boolean isPassageiroDiam\
ante){
if (bilhete.isPrimeiraClasse() && isPassageiroDiamante) {
return new EstrategiaPrimeiraClasseDiamante();
} else if (bilhete.isPrimeiraClasse()) {
return new EstrategiaPrimeiraClasse();
} else if (bilhete.isClasseEconomica()) {
return new EstrategiaClasseEconomica();
} else {
return new EstrategiaPromocional();
}
}
}
Agora basta que o método calcularPontuacao pegue a estratégia do bilhete para definir quantos pontos devem ser dados ao passageiro. A utilização ficaria como mostrado na Listagem 8.
public int calcularPontuacao(Passageiro passageiro) {
int totalDePontos = 0;
List<Voo> voosPendentes = passageiro.getVoosComPontuacaoPendente();
for (Voo voo : voosPendentes) {
Bilhete bilhete = passageiro.getBilhete(voo.getNumero());
EstrategiaDePontuacao estrategia = bilhete.getEstrategiaDePontuacao(passagei\
ro.isDiamante());
totalDePontos += estrategia.calcularPontuacao(voo.getDistancia());
}
passageiro.atualizarPontuacao(totalDePontos);
passageiro.marcarVoosComoProcessados();
return totalDePontos;
}
Ao quebrar o fluxo do algoritmo em estratégias conseguimos simplificar o método calcularPontuacao e dividir a responsabilidade de saber qual regra utilizar com a classe Bilhete. Por sua vez, a lógica de cálculo da pontuação fica definida dentro de cada uma das estratégias.
No final ganhamos maior coesão ao separar as responsabilidades, facilitando modificar e até mesmo adicionar novas regras de pontuação.
Um Pouco de Teoria
Assim como no caso do padrão Simple Factory, ao aplicar o Strategy fica bem claro a divisão das responsabilidades, seguindo o Princípio da Responsabilidade Única. A lógica para calcular a pontuação fica mais simples uma vez que está separada.
Após distribuir os fluxos do algoritmo nas classes estratégias, podemos também mover os testes e fazê-los validar apenas uma parte do código. Considerando que testes são a melhor forma de documentação, uma nova pessoa na equipe conseguirá ver o funcionamento de um fluxo de cada vez, ao invés de todo o algoritmo.
Como a estrutura de estratégias define uma interface comum para todos, também é fácil notar o Princípio da Substituição de Liskov. Trocar as estratégias, ou até mesmo adicionar novas, não vai ter nenhum impacto pois o código que as utiliza continuará lidando com a mesma interface.
O Princípio da Inversão de Dependência também fica claro pois o cliente não usa as estratégias concretas diretamente, apenas uma interface. Assim, cada implementação pode ter suas próprias regras sem interferir na estrutura do código.
Outro ponto importante é que devido ao baixo acoplamento entre a classe, fica mais fácil evoluir os códigos separadamente. Como as estratégias tendem a mudar menos do que o código do cliente, também seguimos a ideia de que uma classe deve depender de outras menos prováveis de mudar.
Quando Não Usar
Semelhante ao padrão Simple Factory, o Strategy é uma excelente maneira de começar a refatorar seu código. Mas, devido a sua simplicidade, eventualmente pode ser necessário partir para soluções mais robustas e evitar que as estratégias cresçam sem limite.
Como em qualquer padrão, é importante entender o contexto do problema para identificar a melhor solução. Se o contexto muda, a solução provavelmente mudará. Existem duas grandes necessidades de contexto para aplicar o Strategy de maneira efetiva: 1) os fluxos do algoritmo podem ser separados de maneira independente e 2) uma vez que sabemos qual caminho seguir, ele não muda até o final da execução do algoritmo.
Nas seções a seguir vamos comparar o Strategy com os padrões Template Method e State, que podem ajudar quando um dos contextos detalhados anteriormente não puder ser cumprido. Não vamos entrar em detalhes sobre a implementação destes padrões, mas recursos com mais detalhes serão indicados ao final do artigo (veja a seção de Referências Externas).
Evoluindo o Strategy para Template Method
Garantir que o fluxo do algoritmo possa ser separado nem sempre é possível. A vezes as regras que você precisa aplicar não vão permitir que o algoritmo seja quebrado em fluxos diferentes. Uma ideia para começar e validar essa possibilidade é tentar duplicar o código entre as estratégias.
Se o algoritmo separado precisar de muita duplicação, talvez a separação em estratégias não seja a melhor opção. Os ganhos obtidos com a flexibilidade da solução não vão compensar os gastos com a manutenção de código repetido. Nesses casos, outros padrões podem ajudar a resolver o problema, como o Template Method.
Ao aplicar o Template Method definimos uma estrutura base, que será comum a todas as variações de fluxos, além de vários pontos gancho onde podemos variar a implementação. Dessa forma os pontos comuns ficam centralizados na classe mãe e as classes filhas podem implementar sua própria lógica se beneficiando do algoritmo base.
Caso fosse necessário aplicar uma mesma regra em todas as diferentes estratégias (por exemplo, incluir um bônus de 10% para primeiras compras), essa regra precisaria ser duplicada. Poderíamos definir um template que calcula a pontuação baseado na distância, regra que seria definida pelas classes filhas, e sempre adiciona essa bonificação extra ao final.
Evoluindo o Strategy para State
Com relação a segunda necessidade de contexto, precisamos garantir que uma vez que a estratégia é definida ela não mudará, já que a principal vantagem de utilizar o Strategy é diminuir a quantidade de decisões que precisam ser tomadas. Se for preciso fazer várias verificações em pontos diferentes para descobrir qual estratégia utilizar, ou modificar a estratégia atual, a aplicação do padrão não ajudará muito.
Na maioria das vezes é preciso revisitar todo o algoritmo para tentar fazer com que essa decisão só aconteça uma vez. No entanto, caso as regras do algoritmo realmente precisem que o fluxo mude no meio do caminho, pode ser melhor aplicar o padrão State.
Assim como no padrão Strategy, o State também sugere dividir os fluxos do algoritmo mas, ao invés de escolher uma estratégia a ser seguida, criamos estados contendo suas informações e regras.
Esses estados serão facilmente trocados, conforme necessário, pois cada um deles saberá qual deve ser o próximo estado a ser chamado, bem semelhante a uma Máquina de Estados Finita. Assim não precisamos nos preocupar em escolher qual estado utilizar, basta configurar o ponto de partida.
Poderíamos evoluir o código do exemplo anterior para o State caso os status do passageiro tivessem uma maior influência (por exemplo, se para cada status um multiplicador de pontuação diferente fosse aplicado). Ao invés de fazer com que cada estratégia adicione vários ifs para saber o que fazer dependendo do passageiro, cada status seria mapeado para um estado que saberia aplicar suas regras bem como quando o passageiro evoluiu para um novo estado.