A importância dos Padrões de Projeto
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:
- Princípios de Design de Software
- Coesão versus Acoplamento
- Programar para Interface e não para Implementação
- Herança
- Padrão de Projeto
- Catálogo de Padrões
- Padrões Criacionais
- Padrões Estruturais
- Padrões Comportamentais
- Strategy
- Conclusão
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.
public class Cliente {
public static void main(String[] args) {
// obtendo conexão com servidor Oracle
ConexaoOracle conexaoOracle = new ConexaoOracle();
conexaoOracle.conectar();
}
}
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”.
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.
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.
- 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;
- O problema descreve quando aplicar o padrão, definindo os problemas que podem ser solucionados e seus contextos;
- 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;
- 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 |
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.
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);
}
}
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.
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.
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.
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.
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.
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;
}
}
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;
}
}
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;
}
}
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.
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();
}
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++);
}
}
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.
public Iterator getIterator() {
return new HomensIterator(homens);
}
public Iterator getIterator() {
return new MulheresIterator(mulheres);
}
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.
public interface Operacao {
int executar(int valor1, int valor2);
}
public class Adicao implements Operacao {
@Override
public int executar(int valor1, int valor2) {
return valor1 + valor2;
}
}
public class Subtracao implements Operacao {
@Override
public int executar(int valor1, int valor2) {
return valor1 - valor2;
}
}
public class Multiplicacao implements Operacao {
@Override
public int executar(int valor1, int valor2) {
return valor1 * valor2;
}
}
public class Divisao implements Operacao {
@Override
public int executar(int valor1, int valor2) {
return valor1 / valor2;
}
}
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;
}
}
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
Artigos relacionados
-
Artigo
-
Vídeo
-
Vídeo
-
DevCast
-
DevCast