No paradigma da programação orientada a objetos, a herança é frequentemente utilizada para reaproveitar código comum em uma superclasse. Contudo, quando o número de subclasses aumenta e os comportamentos começam a divergir significativamente, a simples herança pode levar a um excesso de sobrescritas (overriding), dificultando a manutenção e violando princípios de design como o Open/Closed Principle.
O padrão Strategy resolve esse problema ao definir uma família de algoritmos, encapsulando cada um deles e tornando-os intercambiáveis. Isso permite que o comportamento de um objeto mude dinamicamente através da composição, em vez de depender estritamente da herança.
O Problema da Herança Rígida
Considere um cenário onde temos uma classe base para animais. Inicialmente, definimos um método genérico para alimentação. À medida que adicionamos animais específicos, somos forçados a soberscrever esse método repetidamente.
// Abordagem problemática com herança pura
public class Animal {
public void executarAlimentacao() {
System.out.println("O animal está comendo.");
}
}
class Leao extends Animal {
@Override
public void executarAlimentacao() {
System.out.println("Leão comendo carne.");
}
}
class Zebra extends Animal {
@Override
public void executarAlimentacao() {
System.out.println("Zebra comendo ervas.");
}
}
Refatoração com o Padrão Strategy
Para tornar o sistema flexível, extraímos o comportamento de alimentação para uma interface e criamos implementações concretas para cada tipo de dieta. A classe principal passa a utilizar a composição para delegar a execução desse comportamento.
// 1. Definição da Interface de Estratégia
interface DietaStrategy {
void comer();
}
// 2. Implementações Concretas das Estratégias
class DietaCarnivora implements DietaStrategy {
private String especie;
public DietaCarnivora(String especie) {
this.especie = especie;
}
@Override
public void comer() {
System.out.println(especie + " está caçando e comendo carne.");
}
}
class DietaHerbivora implements DietaStrategy {
private String especie;
public DietaHerbivora(String especie) {
this.especie = especie;
}
@Override
public void comer() {
System.out.println(especie + " está pastando e comendo vegetação.");
}
}
// 3. Classe de Contexto utilizando Composição
public abstract class Animal {
protected DietaStrategy dietaStrategy;
public void setDieta(DietaStrategy dietaStrategy) {
this.dietaStrategy = dietaStrategy;
}
public void realizarAlimentacao() {
if (dietaStrategy != null) {
dietaStrategy.comer();
} else {
System.out.println("Comportamento de dieta não definido.");
}
}
}
// Subclasses simplificadas
class Leao extends Animal {
public Leao() {
// Pode ser definido um padrão ou injetado externamente
}
}
class Girafa extends Animal {
public Girafa() {
}
}
// 4. Demonstração de Uso
public class SistemaZoologico {
public static void main(String[] args) {
Animal leao = new Leao();
leao.setDieta(new DietaCarnivora("Leão Africano"));
leao.realizarAlimentacao();
Animal girafa = new Girafa();
girafa.setDieta(new DietaHerbivora("Girafa Reticulada"));
girafa.realizarAlimentacao();
// Mudança dinâmica de comportamento
System.out.println("--- Alteração dinâmica ---");
leao.setDieta(new DietaHerbivora("Leão em dieta especial"));
leao.realizarAlimentacao();
}
}
Ao adotar essa estrutura, ganhamos a flexibilidade de adicionar novos tipos de alimentação sem modificar as classes de animais existentes. Além disso, diferentes animais podem compartilhar a mesma instância de estratégia ou trocar de comportamento em tempo de execução, promovendo um baixo acoplamanto entre a lógica de negócio e as entidades de dados.