Por que eu devo ler este artigo:Este artigo apresentará os padrões de projeto, com enfoque nos benefícios que podem ser obtidos através da correta utilização dos mesmos.

Somente conhecer e utilizar os princípios básicos da orientação a objetos ou de uma linguagem de programação, não garante o desenvolvimento de softwares flexíveis, reutilizáveis e de fácil manutenção. Os padrões de projeto representam soluções que tentam suprir tais necessidades.


Guia do artigo:

Com a finalidade de proporcionar o reuso da experiência adquirida na solução de problemas corriqueiros, o conceito de padrão de projeto surgiu na construção civil e posteriormente foi adotado pela Engenharia de Software. Entretanto, somente se tornou popular no mundo do desenvolvimento de software após o lançamento do livro Design Patterns: Elements of Reusable Object-Oriented Software, escrito por quatro autores, que posteriormente ficaram conhecidos como a “Gangue dos Quatro”.

Quando utilizados de forma correta, os padrões de projeto colaboram para a obtenção de um design flexível, mais coeso e menos acoplado. Neste artigo, abordaremos os padrões de projeto catalogados pela “Gangue dos Quatro”, com enfoque nos benefícios obtidos através da utilização dos mesmos.

Os padrões de projeto, também conhecidos pelo termo original em inglês design patterns, descrevem soluções para problemas recorrentes no desenvolvimento de software, e quando utilizados de forma correta, refletem diretamente no aumento da qualidade do código, tornando-o mais flexível, elegante e reusável.

A ideia de padrões de projeto não se restringe ao desenvolvimento de software. Ela foi elaborada na década de 70, pelo arquiteto Christopher Alexander, que escreveu o primeiro catálogo de padrões de que se tem conhecimento. Neste catálogo foram descritos padrões em projetos da construção civil.

Somente após o lançamento do livro Design Patterns: Elements of Reusable Object-Oriented Software, em 1995, que os padrões realmente ganharam popularidade no mundo do desenvolvimento de software. Neste livro foram catalogados e descritos vinte e três padrões para o desenvolvimento de software orientado a objetos. Seus autores, Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides, ficaram conhecidos como a “Gangue dos Quatro” (Gang of Four) ou simplesmente GoF. Posteriormente, vários outros livros do estilo foram publicados. Tendo, inclusive, a Sun Microsystems, publicado, em 2002, a primeira edição do livro Core J2EE Patterns: Best Practices and Design Strategies, que catalogou vinte e cinco padrões voltados para o J2EE, utilizando e baseando-se nos padrões GoF.

Neste contexto, a principal vantagem do uso de padrões de projeto está no reuso das soluções propostas para determinado problema, o que permite que até mesmo profissionais menos experientes possam atuar como especialistas. Pois os padrões, geralmente, são frutos da experiência de profissionais experientes que tiveram a oportunidade de aplicar e validar tais soluções em projetos reais. Além disso, podemos destacar a facilitação da manutenção, já que um padrão representa uma unidade de conhecimento comum entre os envolvidos.

A utilização de alguns padrões, apesar de ser benéfica na maioria dos casos, torna o código-fonte maior e mais complexo. Isto nos faz refletir sobre a possibilidade de estarmos desnecessariamente aumentando a complexidade do design. Portanto, é necessário não somente conhecer os padrões de projeto, mas sim, realmente entendê-los para identificar quando utilizá-los e usufruir positivamente da experiência herdada.

Neste artigo iremos abordar os padrões de projeto catalogados pela Gangue dos Quatro, aplicados à linguagem Java. Apesar disso, vale ressaltar que tais padrões podem ser implementados em qualquer linguagem de programação orientada a objetos.

Deste modo, primeiramente analisaremos alguns princípios de design de software, a fim de construir uma base sólida, para logo após apresentar como um padrão de projeto pode ser definido, organizado e catalogado, exemplificar alguns padrões e casos de uso.

Princípios de Design de Software

Como vimos até o momento, os padrões de projeto são soluções robustas e flexíveis que tornam a evolução do software uma tarefa menos dolorosa. Em uma análise mais profunda, pode-se notar que tais benefícios são frutos de alguns princípios da orientação a objetos, que são comuns entre os padrões. Portanto, vamos refletir sobre alguns desses princípios, antes de aprofundarmos nosso estudo sobre os padrões.

Coesão versus Acoplamento

A coesão se refere ao escopo do objeto, ou seja, está diretamente relacionada com as responsabilidades atribuídas ao objeto para que cumpra a sua finalidade. Para obter um design coeso devemos definir claramente o propósito do objeto e centralizar nele tudo o que o diz respeito, evitando que os algoritmos se espalhem pelo projeto dificultando a localização e, consequentemente, a manutenção.

O acoplamento está relacionado com o grau de dependência entre os objetos, ou seja, o conhecimento necessário dos atributos e métodos de uma classe para que a mesma possa ser utilizada por outra e as colaborações possam ocorrer. A flexibilidade é o quesito mais afetado em um design acoplado, pois o mesmo não permite que os elementos da composição de objetos sejam facilmente substituídos sem que o funcionamento seja prejudicado.

Em um projeto de software, coesão e acoplamento devem ser grandezas inversas, sendo que o ideal seria projetar com alta coesão e baixo acoplamento, centralizando as responsabilidades e compondo estruturas flexíveis, de forma a facilitar futuras manutenções.

Programar para Interface e não para Implementação

Uma interface é basicamente um contrato, onde são declaradas as operações que deverão ser suportadas pela classe que a implementar. Através das interfaces, na linguagem Java, é possível colocar em prática o conceito de polimorfismo, onde diferentes objetos que implementam uma interface comum, suportam as mesmas operações, mas possivelmente com implementações diferentes. O polimorfismo resulta em maior flexibilidade para o design, pois desacopla os objetos e permite que eles sejam permutados em tempo de execução (dynamic binding).

Observe o cenário da classe Cliente, apresentada na Listagem 1, que necessita obter conexão com apenas um servidor de banco de dados Oracle através da classe ConexaoOracle, apresentada na Listagem 2.

Listagem 1. Código da classe Cliente.
public class Cliente {
    public static void main(String[] args) {
      // obtendo conexão com servidor Oracle
      ConexaoOracle conexaoOracle = new ConexaoOracle();
     conexaoOracle.conectar();
    }
  }
Listagem 2. Código da classe ConexaoOracle.
public class ConexaoOracle {
    public void conectar() {
      System.out.println("Obtendo conexão no servidor Oracle!");
    }
  }

Apesar de o software funcionar utilizando banco de dados Oracle, alguns potenciais clientes já possuem outros servidores de bancos de dados. Assim, para atendê-los, o software terá de ser capaz de se conectar a diferentes serviços de armazenamento de dados. Neste exemplo os benefícios serão mínimos, mas será possível entender o conceito e os benefícios de se programar voltado para uma interface e não para uma implementação concreta. Para isso, conforme apresentado na Listagem 3, criaremos a interface Conexao, que será o nosso “contrato”.

Listagem 3. Interface Conexao.
public interface Conexao {
    void conectar();
  }

A partir de agora, para suprir as necessidades da classe Cliente, será necessário uma instância de uma classe que implemente a interface Conexao. A flexibilidade advém da possibilidade de mudar a implementação da interface, inclusive em tempo de execução, como demonstrado na Listagem 4.

Listagem 4. Classe Cliente obtendo conexões a diversos servidores de banco de dados.
public class Cliente {
    public static void main(String[] args) {
      Conexao conexao;
   
      // obtendo conexão com servidor Oracle
      conexao = new ConexaoOracle();
      conexao.conectar();
   
      // obtendo conexão com servidor MySQL
      conexao = new ConexaoMySQL();
      conexao.conectar();
   
      // obtendo conexão com servidor Oracle
      conexao = new ConexaoSQLServer();
      conexao.conectar();
    }
  }

Favorecer a Composição sobre a Herança

A utilização da herança, apesar de trazer algumas facilidades, apresenta diversos problemas de acoplamento e dificulta a customização devido ao seu comportamento encadeado, pois quando se faz necessária alguma atualização na superclasse, o comportamento alterado é refletido também para as subclasses.

A herança também pode ser considerada uma má pratica, pois caracteriza a violação do principio do encapsulamento, que rege que os atributos dizem respeito somente a sua própria classe e não devem ser manipulados pelas demais.

Apesar de seu uso não ser recomendado, a herança pode ser utilizada, com bom senso, em casos específicos em que é possível dizer que a subclasse “É UM” tipo da superclasse. Enquanto a composição é empregada quando o objeto, para estender as suas funcionalidades, “TEM UM” outro que o auxilia. A composição estende uma classe através da delegação do trabalho para outro objeto.

A composição é vantajosa devido a agregar uma estrutura independente ao objeto, que pode ser alterada sem se preocupar com a propagação da alteração, e não viola o princípio do encapsulamento. Além de que, através da composição, é possível reproduzir quase todos os recursos da herança.

Definindo Padrão de Projeto

Um padrão de projeto é uma estrutura recorrente que resolve satisfatoriamente um determinado problema em seu contexto. Portanto, é importante estudá-lo a fim de promover o seu reuso.

Segundo o modelo da Gangue dos Quatro, cada padrão possui quatro elementos essenciais: nome, problema, solução e consequências.

  1. O nome é uma maneira de descrever o problema, sua solução e as consequências em uma ou duas palavras. Um identificador que contribui para o aumento do nível de abstração e para a geração de um vocabulário comum, facilitando a comunicação;
  2. O problema descreve quando aplicar o padrão, definindo os problemas que podem ser solucionados e seus contextos;
  3. A solução descreve os elementos que compõem a modelagem, seus relacionamentos e responsabilidades. Ela não descreve uma modelagem ou implementação concreta, mas sim uma solução genérica, visto que um padrão é aplicável a diferentes situações e independente de tecnologia;
  4. As consequências são os resultados e riscos assumidos através da implementação do padrão, onde é possível avaliar os custos e benefícios da solução, e decidir quando utilizá-la.

Organizando o Catálogo de Padrões

Os padrões GoF foram classificados de acordo com dois critérios. O primeiro critério, denominado propósito, reflete o que o padrão faz. Os padrões podem ter propósito criacional, estrutural ou comportamental. Os padrões criacionais abstraem o processo de criação dos objetos. Os estruturais lidam com a composição de classes ou objetos. Já os comportamentais caracterizam as maneiras pelas quais classes ou objetos interagem e distribuem responsabilidades.

O segundo critério, denominado escopo, especifica se o padrão é aplicável principalmente às classes ou aos objetos. Os padrões de classe lidam com o relacionamento entre classes e suas subclasses. Enquanto os padrões de objetos lidam com as relações entre objetos, que podem ser alteradas em tempo de execução e por isso são mais dinâmicos. Veja a Tabela 1.

Propósito

Criacional

Estrutural

Comportamental

Escopo

Classe

Factory Method

Adapter

Interpreter

Template Method

Objeto

Abstract Factory

Builder

Prototype

Singleton

Adapter

Bridge

Composite

Decorator

Facade

Flyweight

Proxy

Chain of Responsibility

Command

Iterator

Mediator

Memento

Observer

State

Strategy

Visitor

Tabela 1. Classificação dos padrões apresentada no catálogo da Gangue dos Quatro.

Padrões Criacionais

Os padrões criacionais, estão ligados à forma pela qual os objetos de uma determinada classe são criados. Tais padrões tornam o software independente de como os objetos são criados e compostos. Devido a isso, proporcionam grande flexibilidade e permitem a configuração do software como produto de objetos que variam em estrutura e funcionalidade.

A seguir, será apresentado um exemplo de padrão criacional.

Singleton

O padrão Singleton, garante a existência de apenas uma instância da classe no ciclo de vida do aplicativo. Pois encapsula a lógica de instanciação da classe e quando solicitado verifica se já não existe uma instancia válida, não sendo necessário e nem mesmo permitido instanciar novamente.

A instância é criada e gerenciada pela própria classe, através de uma referencia estática a si mesma. Para isso, o construtor passa a ser privado para garantir que não seja possível criar objetos de fora da classe. A criação, por sua vez, passa a ser controlada através de um método publico e estático, que se torna o único meio de se obter uma instancia da classe.

Seu uso é recomendado para reduzir a instanciação desnecessária de objetos que seriam utilizados e logo descartados pelo Garbage Collector, e também de objetos considerados “pesados” como, por exemplo, aqueles que realizam interações com o banco de dados.

Para exemplificar, analisaremos na Listagem 5, a classe utilitária ResourceBundleUtils, a qual é empregada na internacionalização de aplicações e provavelmente utilizada em todas as telas da aplicação, sem a real necessidade uma instancia diferente para cada tela. Assim como também será demonstrada na Listagem 6, a obtenção da instancia através da utilização do padrão Singleton.

Garbage Collector: Processo utilizado para automação do gerenciamento de memória, que tem como principio básico encontrar objetos que não serão mais acessados e automaticamente desalocar os recursos utilizados por eles.
Internacionalização: Processo de desenvolvimento e/ou adaptação de um produto, geralmente softwares, para o idioma e cultura de um país.
Listagem 5. Código da classe ResourceBundleUtils.
import java.util.ResourceBundle;

   
  public class ResourceBundleUtils {
   
    private static ResourceBundleUtils instance;
    private ResourceBundle bundle;
   
    private ResourceBundleUtils() {
      bundle = ResourceBundle.getBundle("pt_BR");
    }
   
    public static ResourceBundleUtils getInstance() {
      if (instance == null) {
        instance = new ResourceBundleUtils();
      }
      return instance;
    }
   
    public String getMessage(String key) {
      return bundle.getString(key);
    }
  }
Listagem 6. Exemplo de obtenção de instancia através do padrão Singleton.
public class Contexto {
    public static void main(String[] args) {
      //obtendo instância de ResourceBundleUtils através do padrão Singleton
      ResourceBundleUtils bundle = ResourceBundleUtils.getInstance();
    }
  }

Padrões Estruturais

Os padrões estruturais definem maneiras de se compor objetos para formar estruturas maiores e mais complexas. Ao invés de compor interfaces ou implementações, os padrões estruturais descrevem formas de se compor objetos para realizar novas funcionalidades. A flexibilidade obtida é fruto da possibilidade de alterar a composição em tempo de execução.

A seguir, será apresentado um exemplo de padrão estrutural.

Facade

O padrão Facade representa uma solução elegante na comunicação entre subsistemas, pois centraliza em um único ponto toda a comunicação que ocorre entre eles, reduzindo o acoplamento e facilitando a manutenção.

A estruturação de um sistema em subsistemas ajuda a reduzir a complexidade. Nesse contexto, os esforços se intensificam a fim de minimizar a comunicação e as dependências entre os subsistemas. O que vem ao encontro do objetivo do padrão Facade, visto que o mesmo define uma interface única e de alto nível que torna mais fácil a utilização de um subsistema.

Para exemplificar, analisaremos um cenário onde uma classe Cliente, apresentada na Listagem 7, necessita executar operações de outras classes que fazem parte de um subsistema. Observe o diagrama de classes apresentado na Figura 1.

Diagrama de Classes sem utilização do padrão Facade
Figura 1. Diagrama de Classes sem utilização do padrão Facade.
Listagem 7. Código da classe Cliente.

public class Cliente {

ClasseA classeA = new ClasseA();
   
   ClasseB classeB = new ClasseB();
   
   ClasseC classeC = new ClasseC();
  }

Como foi possível observar, o Cliente acessa diretamente as classes internas ao subsistema. Note que tal relacionamento acopla o design, pois vincula o Cliente diretamente às classes que se relaciona. Podemos diminuir o acoplamento através da inclusão de um objeto intermediário que se comportará como uma fachada e será responsável pela comunicação externa com o subsistema. Observe as modificações no diagrama de classes apresentado na Figura 2.

Diagrama de Classes com utilização do padrão Facade

Figura 2. Diagrama de Classes com utilização do padrão Facade.
Listagem 8. Código da classe Facade.
public class Facade {
   
   public ClasseA getClasseA() {
    return new ClasseA();
   }
   
   public ClasseB getClasseB() {
    return new ClasseB();
   }
   
   public ClasseC getClasseC() {
    return new ClasseC();
   }
  }

Neste simples exemplo, nossa fachada, apresentada na Listagem 8, será responsável apenas pela instanciação das classes integrantes do subsistema. Mas sua utilização pode ser estendida para encapsular complexidades desnecessárias para o Cliente como, por exemplo, conversões de tipo de dados e cálculos. Observe agora, na Listagem 9, como a classe Cliente passará a acessar as classes do subsistema utilizando o padrão Facade, evitando o acesso direto e tornando o design menos acoplado.

Listagem 9. Código da classe Cliente.
public class Cliente {
   public static void main(String[] args) {
   
    // instanciando a fachada
    Facade facade = new Facade();
   
    ClasseA classeA = facade.getClasseA();
   
    ClasseB classeB = facade.getClasseB();
   
    ClasseC classeC = facade.getClasseC();
   
   }
  }

Padrões Comportamentais

Os padrões comportamentais preocupam-se com os algoritmos e a atribuição de responsabilidades entre os objetos. Esses padrões são caracterizados pelo complexo controle de fluxo, difícil de acompanhar em tempo de execução, mas permitem que o desenvolvedor se concentre apenas em como os objetos estão interligados.

A seguir, serão apresentados alguns exemplos de padrões comportamentais.

Iterator

O padrão Iterator provê uma forma de sequencialmente acessar objetos de uma coleção, sem expor sua implementação ou estrutura interna. Para isso, propõe a centralização de todas as regras de navegação da coleção em uma classe, de forma que tais regras possam variar sem que seja necessário expor os detalhes de nossas coleções.

A linguagem Java possui suporte nativo ao padrão Iterator através da interface java.util.Iterator, e o utiliza amplamente para manipular coleções no Java Collections Framework.

Java Collections Framework: Framework presente desde a versão 1.2 do JDK, que abrange um conjunto de classes e interfaces que implementam estruturas de dados de coleções comumente reutilizáveis.

Para demonstrar a eficácia deste padrão, vejamos um exemplo em que o objetivo é listar os itens de duas coleções compostas por elementos do tipo Pessoa (vide Listagem 10). Para enfatizar que não há necessidade de expor nossas coleções ao usuário, utilizaremos duas estruturas de dados distintas: um ArrayList na Listagem 11, e um vetor na Listagem 12. Contudo, inicialmente será apresentada uma versão sem a implementação do padrão Iterator, para que seja possível adicioná-lo e assim observar as diferenças.

Listagem 10. Código da classe Pessoa.
public class Pessoa {
   private Integer codigo;
   private String nome;
   
   public Pessoa(Integer codigo, String nome) {
    this.codigo = codigo;
    this.nome = nome;
   }
   
   public Integer getCodigo() {
    return codigo;
   }
   
   public void setCodigo(Integer codigo) {
    this.codigo = codigo;
   }
   
   public String getNome() {
    return nome;
   }
   
   public void setNome(String nome) {
    this.nome = nome;
   }
  }

Listagem 11. Código da classe Homens.
public class Homens {
   private ArrayList<Pessoa> homens;
   
   public Homens() {
    homens = new ArrayList<Pessoa>();
   
    adicionarPessoa(1, "Mario");
    adicionarPessoa(2, "João");
    adicionarPessoa(3, "José");
    adicionarPessoa(4, "Fernando");
    adicionarPessoa(5, "Carlos");
   }
   
   public void adicionarPessoa(Integer codigo, String nome) {
    Pessoa homem = new Pessoa(codigo, nome);
    homens.add(homem);
   }
   
   public ArrayList<Pessoa> getPessoas() {
    return homens;
   }
  }
Listagem 12. Código da classe Mulheres.
public class Mulheres {
   private Pessoa[] mulheres;
   
   public Mulheres() {
    mulheres = new Pessoa[5];
    
    adicionarPessoa(0, 1, "Maria");
    adicionarPessoa(1, 2, "Joana");
    adicionarPessoa(2, 3, "Camila");
    adicionarPessoa(3, 4, "Fernanda");
    adicionarPessoa(4, 5, "Carla");
   }
   
   public void adicionarPessoa(Integer indice, Integer codigo, String nome) {
    Pessoa mulher = new Pessoa(codigo, nome);
    mulheres[indice] = mulher;
   }
   
   public Pessoa[] getPessoas() {
    return mulheres;
   }
  }
Listagem 13. Código da classe RelatorioDePessoas.
public class RelatorioDePessoas {
    public static void main(String[] args) {
   
      /**
       * Obtendo ArrayList de Homens
       */
      Homens arrayListHomens = new Homens();
      ArrayList<Pessoa> homens = arrayListHomens.getPessoas();
   
      /**
       * Listando pessoas do ArrayList
       */
      for (int i = 0; i < homens.size(); i++) {
        Pessoa homem = homens.get(i);
        System.out.println(homem.getCodigo() + " - " + homem.getNome());
      }
   
      /**
       * Obtendo vetor de Mulheres
       */
      Mulheres vetorMulheres = new Mulheres();
      Pessoa[] mulheres = vetorMulheres.getPessoas();
   
      /**
       * Listando pessoas do vetor
       */
      for (int i = 0; i < mulheres.length; i++) {
        Pessoa mulher = mulheres[i];
        System.out.println(mulher.getCodigo() + " - " + mulher.getNome());
      }
   
    }
  }

Como podemos observar na Listagem 13, além de expor nossas coleções e suas estruturas internas, foi necessária a repetição de dois trechos de código praticamente iguais.

Vejamos agora como a implementação do padrão irá tornar o nosso código mais simples e elegante. Para isso, criaremos a nossa própria interface Iterator (vide Listagem 14), que irá conter a assinatura de apenas dois métodos para controlar o acesso aos itens das coleções. Nas Listagens 15 e 16, são apresentadas as implementações desta interface para encapsular as regras de navegação das distintas estruturas de dados.

Listagem 14. Interface Iterator.
public interface Iterator {
   /**
    * Método que irá verificar a existência de um próximo item na coleção
    */
   boolean hasNext();
   
   /**
    * Método que irá retornar um item da coleção
    */
   Object next();
  }
Listagem 15. Código da classe HomensIterator.
public class HomensIterator implements Iterator {
   
   private ArrayList<Pessoa> homens;
   private int indice = 0;
   
   public HomensIterator(ArrayList<Pessoa> homens) {
    this.homens = homens;
   }
   
   /**
    * Método que verifica a existência de um próximo item no ArrayList
    */
   @Override
   public boolean hasNext() {
    return (homens.size() > indice);
   }
   
   /**
    * Método que retorna o item localizado em determinada posição do ArrayList
    */
   @Override
   public Object next() {
    return homens.get(indice++);
   }
  }
Listagem 16. Código da classe MulheresIterator.
public class MulheresIterator implements Iterator {
   
    private Pessoa[] mulheres;
    private int indice = 0;
   
    public MulheresIterator(Pessoa[] mulheres) {
      this.mulheres = mulheres;
    }
   
    /**
     * Método que verifica a existência de um próximo item no vetor
     */
    @Override
    public boolean hasNext() {
      return (mulheres.length > indice);
    }
   
    /**
     * Método que retorna o item localizado em determinada posição do vetor
     */
    @Override
    public Object next() {
      return mulheres[indice++];
    }
   
  }

Agora que já possuímos uma implementação da interface Iterator para cada estrutura de dados utilizada, será necessário desenvolvermos um meio para acessá-las a fim de manipular nossas coleções. Deste modo, para tornar possível o acesso à implementação de Iterator correspondente, acrescentaremos em nossas coleções (Listagens 11 e 12) o método getIterator(), que será responsável por criar e nos retornar os objetos de interação relacionados à coleção, conforme demonstrado nas Listagens 17 e 18.

Listagem 17. Método acrescentado na classe Homens.
public Iterator getIterator() {
    return new HomensIterator(homens);
   }
Listagem 18. Método acrescentado na classe Mulheres.
public Iterator getIterator() {
    return new MulheresIterator(mulheres);
   }
Listagem 19. Classe RelatorioDePessoas utilizando o padrão Iterator.
public class RelatorioDePessoas {
   
   public void imprimirPessoas(Iterator iterator) {
    while (iterator.hasNext()) {
     Pessoa pessoa = (Pessoa) iterator.next();
     System.out.println(pessoa.getCodigo() + " - " + pessoa.getNome());
    }
   }
   
   public static void main(String[] args) {
    RelatorioDePessoas relatorio = new RelatorioDePessoas();
    relatorio.imprimirPessoas(new Homens().getIterator());
    relatorio.imprimirPessoas(new Mulheres().getIterator());
   }
  }

Note que as duas implementações (Listagens 13 e 19) apresentadas exibem resultados exatamente iguais. Porém, na Listagem 19, evitamos a repetição desnecessária de código e encapsulamos as nossas coleções com a implementação do padrão Iterator. A partir de agora, quem utilizar nossas coleções apenas terá conhecimento de que elas são compostas através da classe Pessoa.

Strategy

O padrão Strategy define uma maneira de encapsular uma família de algoritmos, também conhecidos por estratégias, e os torna intercambiáveis. Isto permite que o algoritmo varie independentemente dos Clientes que o utilizam.

Para exemplificar, implementaremos uma Calculadora e suas quatro operações básicas: adição, subtração, multiplicação e divisão. Para tal, começaremos utilizando o princípio de programar para interface, criando, na Listagem 20, a interface Operacao. O uso desta interface irá nos proporcionar a possibilidade de alternar dinamicamente entre as estratégias através do polimorfismo. Depois de definida a interface, é preciso identificar o que tende a variar e separar do que é estático, ou seja, definir as estratégias e fazê-las implementar a interface que criamos, sendo que cada estratégia deve ser implementada em uma classe separada. Desta maneira, as estratégias poderão ser facilmente permutadas. Em nosso exemplo, as estratégias serão as operações da Calculadora. Como é possível observar nas Listagens 21, 22, 23 e 24, essas estratégias definem os algoritmos das operações suportadas. Agora observe como elas serão permutadas dinamicamente através da classe DeterminaOperacao (vide Listagem 25) e a utilização das mesmas na classe Calculadora, através do código apresentado na Listagem 26.

Listagem 20. Interface Operacao.
public interface Operacao {
   int executar(int valor1, int valor2);
  }
Listagem 21. Código da classe Adição.
public class Adicao implements Operacao {
   
    @Override
    public int executar(int valor1, int valor2) {
     return valor1 + valor2;
    }
   
   }
Listagem 22. Código da classe Subtracao.
public class Subtracao implements Operacao {
   
    @Override
    public int executar(int valor1, int valor2) {
     return valor1 - valor2;
    }
   
   }
Listagem 23. Código da classe Multiplicacao.
   public class Multiplicacao implements Operacao {
   
    @Override
    public int executar(int valor1, int valor2) {
     return valor1 * valor2;
    }
   
   }
Listagem 24. Código da classe Divisao.
public class Divisao implements Operacao {
   
    @Override
    public int executar(int valor1, int valor2) {
     return valor1 / valor2;
    }
   
   }
Listagem 25. Código da classe DeterminaOperacao.
public class DeterminaOperacao {
   
   private Operacao operacao;
   
   public DeterminaOperacao(Operacao estrategia) {
    this.operacao = estrategia;
   }
   
   int executar(int valor1, int valor2) {
    return operacao.executar(valor1, valor2);
   }
   
   void trocaAlgoritmo(Operacao novaEstrategia) {
    this.operacao = novaEstrategia;
   }
  }
Listagem 26. Código da classe Calculadora.
public class Calculadora {
   
   public DeterminaOperacao determinaOperacao = new DeterminaOperacao(new Adicao());
   
   public void main(String[] args) {
    System.out.println("Efetuando Adição (2+3)");
    System.out.println("2 + 3 = " + determinaOperacao.executar(2, 3));
   
    System.out.println("Efetuando Subtração (3-2)");
    determinaOperacao.trocaAlgoritmo(new Subtracao());
    System.out.println("3 - 2 = " + determinaOperacao.executar(3, 2));
   
    System.out.println("Efetuando Multiplicação (3*2)");
    determinaOperacao.trocaAlgoritmo(new Multiplicacao());
    System.out.println("3 * 2 = " + determinaOperacao.executar(3, 2));
   
    System.out.println("Efetuando Divisão (3/3)");
    determinaOperacao.trocaAlgoritmo(new Divisao());
    System.out.println("3 / 3 = " + determinaOperacao.executar(3, 3));
   }
  }

Conclusão

Podemos fazer uma analogia entre os padrões de projeto e o jogo de xadrez, onde primeiro aprende-se as regras do jogo para depois estudar os movimentos dos grandes jogadores. O mesmo acontece no desenvolvimento de software, onde primeiramente aprendemos os conceitos básicos para depois refinarmos nosso conhecimento através dos bons exemplos.

Os padrões de projeto não se resumem aos padrões catalogados pela Gangue dos Quatro, porém foram eles que abriram caminho para que os padrões se tornassem populares no desenvolvimento de software.

Mais importante do que saber implementar um padrão, é realmente entender como e qual problema determinado padrão resolve. A principal causa de insucesso na aplicação dos padrões é o desconhecimento do seu real propósito. Por isso, os padrões de projeto devem ser utilizados somente quando houver um problema que justifique o seu uso.

Referências

Design Patterns: Elements of Reusable Object-Oriented Software, GAMMA, E.; HELM, R.; JOHNSON, R.; VLISSIDES J., Addison-Wesley, 1995
Catálogo de padrões da Gangue dos Quatro.

Padrões de Projeto: Soluções Reutilizáveis de Software Orientado a Objetos, GAMMA, E.; HELM, R.; JOHNSON, R.; VLISSIDES J., Bookman, 2000
Tradução em língua portuguesa do catálogo de padrões da Gangue dos Quatro.

Core J2EE Patterns: Best Practices and Design Strategies, DEEPAK, A.; CRUPI, J.; MALKS, D., 2ª Ed., Person, 2003
Catálogo de padrões J2EE.

Core J2EE Patterns: As Melhores Práticas e Estratégias de Design, DEEPAK, A.; CRUPI, J.; MALKS, D., 2ª Ed., Campus, 2004
Tradução em língua portuguesa do catálogo de padrões J2EE.

Head First – Design Patterns, FREEMAN, Erich; FREEMAN, Elisabeth, O’Reilly Media, 2004
Livro bastante didático, da série “Use a Cabeça!”, sobre padrões de projeto.

Use a Cabeça! - Padrões de Projeto, FREEMAN, Erich; FREEMAN, Elisabeth, Alta Books, 2007
Tradução em língua portuguesa do livro bastante didático, da série “Use a Cabeça!”, sobre padrões de projeto.

Confira também