Esse artigo faz parte da revista Java Magazine edição 20. Clique aqui para ler todos os artigos desta edição

jm20_capa.jpg

Clique aqui para ler esse artigo em PDF.imagem_pdf.jpg

Design Patterns Aplicados

Padrões e Refactorings Passo a Passo

Mesmo após estudar e entender padrões de projeto resta um desafio: como aplicá-los em projetos reais, em tempo hábil e sem prejudicar o código legado?

Definir da melhor forma padrões de projeto (design patterns) tem sido alvo de debate constante desde a década de 80, quando surgiu o movimento de padrões de projeto de software. A definição original do arquiteto e professor Christopher Alexander no seu livro A Timeless Way Of Building (Oxford University Press, 1979) nos serve bem: “Cada padrão é uma regra de três partes que expressa a relação entre um contexto, um problema e uma solução”.

Sendo assim, para entender um padrão precisamos estudar suas partes: o problema, a solução e o contexto onde é aplicável. Resumidamente, os padrões apresentam soluções para problemas que ocorrem de maneira semelhante, mesmo para projetos em áreas completamente diferentes. No caso de padrões de software orientado a objetos, tais problemas costumam ser a criação de objetos, estruturação de classes, modos de acesso a dados, formas de troca de mensagens e outros que enfrentamos de maneira similar em diversos sistemas. Tendo o problema definido, precisamos analisar o contexto em que ele se manifesta; alguns fatores relacionados ao ambiente, como requisitos não-funcionais, podem determinar se um dado padrão de projeto é aplicável.

Os padrões costumam ser encontrados em catálogos voltados para os contextos e o tipo de problema aos quais se aplicam. Por exemplo, o clássico livro Design Patterns [GoF] fornece um catálogo geral de padrões de projeto orientados a objetos; Patterns of Enterprise Application Architecture [PoEAA] traz padrões para aplicações corporativas, enquanto Core J2EE Patterns [J2EE] tem enfoque nos mecanismos e tecnologias do J2EE.

Padrões via refactoring

Escolher os padrões a serem usados em um projeto no seu início requer conhecimento dos principais catálogos, uma boa definição dos requisitos e algum tempo para montar uma arquitetura de avaliação. Não é fácil balancear esse trabalho, que pode representar o investimento em uma boa base para a construção, ou mesmo levar a atrasos que ameacem o projeto. Cada processo de desenvolvimento tem sua maneira de lidar com tais questões, seja numa “fase de elaboração” preliminar, ou através de alterações constantes e controladas ao longo do projeto. Neste artigo, abordaremos a segunda alternativa.

Introduzir padrões de projeto no código existente pode significar uma longa “cirurgia” no sistema, usando tempo e recursos que poderiam ser gastos na implantação de novas funcionalidades ou correção de erros. Entretanto, evitá-los pode deixar o sistema inflexível ou com defeitos de projeto. Uma solução para esse impasse são os refactorings ("refatorações"), que são melhorias na arquitetura existente, desde alterações de nível mais alto como uma reestruturação de pacotes, até "micro-mudanças" como alterações de nomes de atributos.

As refatorações têm sempre um objetivo claro e uma seqüência de passos bem definida, o que em vários casos possibilita a sua execução automática por IDEs e plug-ins. Refatorações costumam ser registradas em catálogos similares aos de padrões de projetos. O mais famoso e um dos pioneiros é o livro Refactoring – Improving the design of Existing Code, por Martin Fowler [Refactoring]. Há também catálogos que misturam padrões e refatorações, como [J2EE] e [RtP].srrigindo os errros existenesacado agressivo.____________________________________________________

Aplicabilidade de patterns

Patterns geralmente fazem parte dos mecanismos centrais de uma aplicação, e estão diretamente ligados a seus requisitos não-funcionais como performance, escalabilidade, manutenibilidade e segurança. Por exemplo, usar o pattern Flyweight [GoF] pode reduzir muito a quantidade de objetos em memória através do compartilhando eficaz de instâncias, melhorando a performance e a escalabilidade; já a utilização do padrão Bridge [GoF] isola a interface de uma classe da sua implementação, permitindo que as duas evoluam independentemente, aumentando assim a manutenibilidade.

Devemos também considerar os custos da solução adotada. Ao escolher um padrão que aumente a flexibilidade do sistema, pode-se deixar o sistema mais complexo ou mais lento. Algumas perdas geralmente valem a pena, mas subestimar os efeitos colaterais da adoção de patterns é um erro comum ao projetar software (veja um caso extremo no quadro "Arruinado por patterns").

Como regra geral, podemos dizer que o esforço para se aplicar uma solução é proporcional à sua generalidade. Os catálogos trazem uma lista de conseqüências comuns da aplicação de cada padrão, baseando-se na experiência dos autores, o que torna a decisão de adotá-los ou não mais fácil e segura.

Exemplo

Para demonstrar a aplicação de padrões de projeto, abordaremos a criação da parte de acesso a dados de um sistema hipotético de pedidos. Nosso exemplo foca na arquitetura e não em questões como controle de transações, pooling de conexões ou performance de consultas. E os testes de unidade, apesar de essenciais para garantir a segurança dos refactorings, foram propositalmente negligenciados para manter o foco nas decisões de projeto.

Vamos partir da implementação mais simples que nos atenda e depois introduzir gradualmente requisitos que motivem refactorings e o uso de padrões de projeto. O padrão central do exemplo será o Data Access Object (DAO) [J2EE]. De forma simplificada, um DAO é um objeto intermediário[1], responsável pelo acesso a dados, que encapsula detalhes de um determinado banco de dados (ou de outro mecanismo de persistência).

O ZIP disponível para download no site da Java Magazine contém o código das três versões do exemplo, em diretórios separados. Feita a descompactação (e tendo o Apache Ant instalado), você pode executar qualquer versão entrando no diretório correspondente e simplesmente executando ant. O exemplo usa como banco de dados o Borland JDataStore, um SGBD relacional embarcado similar ao HSQLDB. A versão final do exemplo suporta também a persistência em arquivos XML, bastando configurar a opção desejada no arquivo versao3.properties.

 

Nota: o código de processamento de XML no exemplo usa as classes java.beans.XMLDecoder e java.beans.XMLEncoder, disponíveis a partir do J2SE 1.4.

Versão 1: o mais simples possível

Na primeira versão temos o projeto mais simples possível, com as entidades principais –  Cliente e Pedido – controlando sua própria persistência. O ciclo de vida (criação e destruição) e a persistência de objetos ItemPedido são controlados pelo Pedido correspondente. Como alternativa, poderíamos ter ItemPedido como classe interna de Pedido, mas optamos pela maior simplicidade de classes independentes. A estrutura da primeira versão é ilustrada na Figura 1.

Mesmo simples, este exemplo já usa o padrão Row Data Gateway [PoEAA], que associa cada objeto a um registro do banco de dados. Entretanto, notamos um primeiro problema. Os métodos de acesso a dados e a lógica de negócio estão na mesma classe – mudar um deles irá requerer a recompilação da classe. Além disso, os métodos de acesso usam a sintaxe do SQL de um banco específico (no caso, o JDataStore), amarrando (ou acoplando) as classes de negócios a esse banco.

Para reduzir o acoplamento, criaremos duas classes para cada entidade "forte" do sistema usando o refactoring "Extract Class" [Refactoring]. Por exemplo, para Cliente haverá a classe de acesso a dados correspondente, ClienteDAO; usaremos o refactoring "Move Method" para mover os métodos de acesso a dados para essa nova classe. Após alterar a classe Main (nosso programa principal) para usar DAOs, podemos remover os métodos de acesso a dados das classes de negócio, pois não temos mais referências a eles.

Mas geramos outro problema: como criar os objetos de acesso a dados sem levar a uma dependência entre o programa principal e a implementação concreta do DAO? Uma solução é usar o padrão Factory Method. Com este, em vez de usar o operador new para criar objetos, utilizamos um outro objeto, responsável por “fabricá-los".

Analisando nosso projeto de exemplo, vemos que não precisamos de várias instâncias de cada DAO ou da fábrica de objetos. Por exemplo, uma única instância de ClienteDAO pode ser usada para persistir todos os clientes. Podemos garantir que uma classe tenha apenas uma instância usando o padrão Singleton [GoF]. Consulte os quadros com título "Pattern explicado:..." para mais detalhes sobre o Factory Method e o Singleton, além dos demais padrões apresentados nos tópicos a seguir.

Após essas modificações, chegamos à "Versão 2".

Versão 2: DAO usando Factory Method

Na Versão 2 temos uma estrutura de DAOs como mostrado nas Figuras 2a e 2b. Perceba que a classe ...

Quer ler esse conteúdo completo? Tenha acesso completo