Padrões de Projeto em Java

Propaganda
Modelo de Artigo – Revista MundoJava
Padrões de Projeto em Java
Reutilizando o projeto de software
“Peter Jandl Jr.” ([email protected])
Atualmente não se concebe um processo de desenvolvimento de software sério sem a
utilização da orientação a objetos, pois esta permite agregar aos sistemas desenvolvidos
sob seus paradigmas qualidades importantes como a extensibilidade e a reusabilidade [5].
Mas sua aplicação, por si só, não garante a obtenção destas qualidades, dado que parece
depender um pouco das linguagens e ferramentas empregadas no desenvolvimento e
teste; muito das técnicas usadas nas etapas de análise e definição destes sistemas; e ainda
mais das concepções de seus projetistas. Como é usual que a experiência dos projetistas
se mostre como fator preponderante no sucesso de qualquer projeto, é muito desejável
compartilhar e transmitir o conhecimento inerente a estas experiências para outros
profissionais, sejam eles iniciantes ou não. Mas como fazer isto? Uma resposta para esta
questão se encontra na utilização dos padrões de projeto.
Introdução
Analisando muitos casos de desenvolvimento, é possível notar que vários dos problemas
endereçados são compreendidos apenas superficialmente, sem que sejam explorados em toda
sua profundidade. Também é comum que a documentação relacionada tanto ao problema
como à solução encontrada esteja incompleta ou ausente. Portanto, uma parcela do problema
permanece sem análise, enquanto que uma parte do conhecimento e experiência adquiridos
nestes projetos fica retida apenas por seus participantes, dificultando seu compartilhamento e
transmissão para outros. Desta forma, problemas idênticos que se repetem em outros
contextos não são reconhecidos como tal, consumindo tempo e recursos para a definição de
soluções que já haviam sido encontradas.
Os padrões de projeto, ou design patterns, vem despertando interesse da comunidade de
projetistas de software por proporcionar elementos que conduzem ao reaproveitamento de
soluções para projetos e não apenas à reutilização de código. Os padrões do projeto permitem
evidenciar os aspectos essenciais de problemas comuns, levando a sua compreensão mais
ampla, e orientando a construção de sistemas que exibam efetivamente as qualidades
desejadas. Através da implementação de alguns padrões de projeto em Java podemos verificar
as vantagens de sua utilização, ao mesmo tempo que passamos a conhecer soluções robustas
para certos problemas. Como motivação adicional destacamos que a plataforma Java se
beneficia do uso intensivo dos padrões de projeto [4].
Definindo os Padrões de Projeto
Um padrão de projeto sistematicamente denomina, motiva e explica uma solução genérica de
projeto, aplicável a um problema recorrente no projeto de sistemas orientados a objetos,
descrevendo assim o próprio problema, a solução, as aplicações e conseqüências de sua
adoção [3]. Esta definição enfatiza o entendimento dos aspectos fundamentais do problema,
motivando o projeto na direção de uma solução genérica e reutilizável, pois cada padrão é
responsável pela solução de um tipo de problema que ocorre repetidas vezes em nosso
ambiente. Deste modo, soluções corretas podem ser aperfeiçoadas e usadas novamente por
outros desenvolvedores em situações semelhantes.
O desafio é possibilitar a "transferência" de conhecimentos dos projetistas mais experientes
para os "novatos", acelerando seu amadurecimento profissional e ampliando as chances de
criação de novas soluções. Projetistas experientes evitam a construção de soluções do início
ao fim, procurando reutilizar as partes semelhantes identificadas em sistemas existentes, de
modo que a cada ciclo de uso destas soluções sejam incorporadas novas características que as
tornem melhor ou mais genéricas. Mas isto só pode ser feito por profissionais maduros, que já
tenham passado por tais experiências, e desde que exista documentação apropriada.
Grande parte das metodologias de projeto enfatizam a solução em si ("o que" e "como" fazer),
deixando de lado o "por que" da solução [1], fazendo que a documentação produzida não seja
útil para compartilhar o conhecimento adquirido. Mais importante do que a própria solução é
a descrição do problema e da forma com que uma solução torna-se aplicável ao mesmo,
incluindo-se nisto as limitações e conseqüências de seu emprego. Os padrões de projeto
pretendem preencher estas lacunas, tornando-se um mecanismo para a comunicação e o
compartilhamento de conhecimento entre desenvolvedores, constituindo uma linguagem
capaz de exprimir mais do que simples estruturas de dados, módulos ou objetos, mas a
articulação destes elementos em soluções arquitetônicas para o software [3]. Quando um
projetista se familiariza com vários padrões de projeto, ele adquire uma parcela da experiência
daqueles que os desenvolveram; e com seu emprego evita reinventar soluções existentes,
permitindo concentrar esforços nos aspectos inéditos do problema e tornando os sistemas
assim desenvolvidos provavelmente mais robustos e confiáveis.
Um Breve Histórico
Em 1977 Christopher Alexander publica um catálogo contendo mais de 250 padrões
construtivos, que discutiam questões comuns da arquitetura, descrevendo em detalhe o
problema e as justificativas de sua solução. Algum tempo depois ele formaliza seu método de
descrição de padrões e o racional de sua aplicação, defendendo que seu uso não limitaria os
arquitetos às soluções prescritas, mas garantiria a presença dos elementos fundamentais que
exprimiam conceitos atemporais de qualidade. Com esta inspiração, Ward Cunnigham e Kent
Beck, criaram cinco padrões voltados para o desenvolvimento de interfaces de usuário, que
proporcionaram grandes ganhos ao projeto onde foram aplicados. Tais resultados,
apresentados em 1987, chamaram a atenção de muitos integrantes da comunidade de software,
fazendo que o tema ganhasse destaque cada vez maior nas conferências sobre orientação a
objetos. Erich Gamma, Richard Helm, Raph Johnson e John Vlissides, que posteriormente se
tornariam conhecidos como a GoF (Gang of Four), começam a trabalhar juntos, publicando
em 1995 o livro "Design Patterns: Elements of Reusable Object-Oriented Software" [3], até
hoje uma referência absoluta no tema. Desde então os padrões de projeto se tornaram
conhecimento essencial para projetistas de software.
Características dos Padrões de Projeto
Embora um padrão de projeto seja a descrição de um problema, de uma solução genérica e sua
justificativa, isto não significa que qualquer solução conhecida para um problema constitua
um padrão, pois existem características que sempre devem ser atendidas pelos padrões [2][3]:
• devem descrever e justificar a solução para um problema concreto e bem definido;
• a solução descrita deve ser comprovada previamente;
• devem descrever relações entre conceitos, mecanismos e estruturas existentes nos
sistemas; e
• o problema tratado deve ocorrer em diferentes contextos, ou seja, se a solução não tem
aplicação em diferentes situações então não constitui um padrão.
Percebemos que os padrões de projeto sustentam uma gama ampla de conceitos: permitem
exprimir adequadamente concepções e estruturas que não são necessariamente objetos;
capturam a evolução e aprimoramento das soluções, equilibrando seus pontos fortes e fracos;
reúnem experiência em torno da solução de um problema comum; e usualmente não
constituem soluções triviais. Também devem ser possível sua utilização independente ou em
conjunto com outros padrões, compondo assim linguagens de padrões.
Descrevendo Padrões de Projeto
Os padrões de projeto podem constituir um catálogo de soluções testadas, aprovadas e
reutilizáveis, definindo um vocabulário especializado onde cada padrão é associado aos
conceitos relativos a um tipo de problema e sua solução. Assim é necessário estruturar sua
documentação, sem se prender às linguagens de programação ou contextos particulares, sendo
freqüente sua descrição textual através de um roteiro padronizado conhecido como forma.
Existem diversas formas (tais como Alexander, Gamma, Coplien etc.) que geralmente
incluem: nome do padrão de projeto, propósito, problema, contexto, dificuldades, limitações
ou restrições, solução, resultados, diagramas e exemplos [4]. O nome do padrão é um
elemento importante, pois será adotado pelos desenvolvedores como uma referência descritiva
do problema, sua solução, características e conseqüências. As descrições associadas ao
propósito, motivação, aplicabilidade e conseqüências devem ser valorizadas, pois contêm o
problema, racional da solução, recomendações e decorrências de seu uso. A estrutura dos
padrões, seus participantes e inter-relacionamentos podem ser descritas através de diagramas
UML. Também é útil listar outros padrões relacionados e situações reais de aplicação.
Exemplos de Padrões de Projeto
Existem muitos padrões de projeto, aplicáveis em domínios diferentes da computação, mas
nenhum estudo inicial sobre o assunto pode deixar de tratar, pelo menos alguns, dos 23
padrões de projeto propostos pela GoF (na Tabela 1 lista exemplos dos mais utilizados). Aqui
detalharemos os padrões Singleton, Factory Method e Facade, muito úteis e essenciais para o
entendimento de outros padrões, utilizando uma forma bastante resumida [4].
Tabela 1. Principais Padrões de Projeto da GoF
Nome do Padrão Descrição Resumida
Abstract Factory Permite criar famílias de objetos sem que suas classes sejam conhecidas diretamente.
Bridge
Separa uma abstração de sua implementação, tornando-as independentes.
Command
Separa o acionador da ação, suportando operações complexas.
Composite
Agrupa objetos em árvores, representando hierarquias do tipo todo/parte.
Decorator
Possibilita a adição dinâmica de novas capacidades a objetos.
Facade
Provê uma interface única e simples para um subsistema complexo com várias interfaces.
Factory Method
Permite criar um objeto sem que sua classe seja diretamente conhecida.
Iterator
Oferece um meio padronizado para acessar elementos de uma estrutura de dados.
Memento
Possibilita salvar o estado de um objeto de modo que o mesmo possa ser restaurado.
Observer
Define um mecanismo de notificação para múltiplos objetos que dependem de um outro.
Singleton
Garante a criação de um único objeto de uma classe específica.
State
Cria objetos cujo comportamento se altera conforme seu estado.
Strategy
Permite que uma família de algoritmos seja utilizada de modo independente e seletivo.
Visitor
Define operações independentes a serem realizadas sobre elementos de uma estrutura.
Singleton
Motivação, propósito e aplicabilidade
É um padrão de projeto simples que ilustra várias características necessárias aos padrões de
projeto. É considerado um padrão de criação, pois está relacionado com o processo de criação
de objetos, possuindo inúmeras aplicações e implementação bastante direta. O que motiva sua
existência é o fato de que muitas aplicações necessitam garantir a ocorrência de uma única
instância de classes específicas, pois tais objetos podem fazer uso de recursos cuja utilização
deve ser exclusiva, ou porque desejamos que os demais elementos do sistema compartilhem
um único objeto particular. Estas situações ocorrem quando vários subsistemas utilizam um
único arquivo de configuração, permitindo sua modificação concorrente; ou quando só
devemos estabelecer uma conexão exclusiva com um banco de dados ou um sistema remoto,
devendo ser compartilhada por várias tarefas paralelas; dentre muitas outras. Ao mesmo
tempo não queremos que os usuários destas classes zelem por esta condição de unicidade, mas
desejamos oferecer acesso simples à instância única que deverá existir, portanto
adicionaremos estas responsabilidades às classes que deverão constituir um Singleton. Assim
este padrão tem como propósito garantir a existência de uma instância única de uma classe
específica, a qual possa ser acessada de maneira global e uniforme.
Estrutura, participantes e conseqüências
A estrutura do Singleton, ilustrada na Figura 1, indica as características necessárias para que a
classe desejada possua uma única instância. Percebemos que tal classe deve manter uma
referência para uma instância de seu próprio tipo e também deve implementar uma operação
“getInstance()” responsável pela criação de apenas um objeto e pelo retorno da referência
correspondente. Instâncias de classes que implementam o padrão Singleton só poderão ser
obtidas através desta operação.
Figura 1. Estrutura do padrão de projeto Singleton
Implementação e exemplo
A implementação em Java deste padrão, deve utilizar um campo privado e estático do tipo da
própria classe para armazenar a referência da única instância permitida, o qual deve ser
inicializado de modo a indicar a inexistência de tal instância. Um método estático, cujo tipo
de retorno também é a própria classe, provê o ponto único de acesso a tal instância. Se a
operação de instanciação ocorrer apenas na primeira solicitação de um objeto da classe,
garantimos a instância única ao mesmo tempo que a criação só ocorrerá quanto estritamente
necessário (lazy instantiation).
Como o Java suporta a clonagem de objetos, devemos declarar a classe como final, para
impedir que suas subclasses possam implementar a interface Cloneable, possibilitando a
duplicação de objetos através do método “clone()”, o que romperia com as obrigações deste
padrão. Caso a classe Singleton seja uma subclasse de outra que suporte a clonagem, então o
método “clone()” deve ser sobreposto por outro que apenas lance a exceção
CloneNotSupportedException, indicando a impossibilidade da realização desta operação.
Na Listagem 1 temos a implementação da classe DBConnection destinada a fornecer uma
conexão única para um banco de dados específico, que obedece a caracterização que fizemos
do padrão Singleton. Através do uso desta classe podemos reduzir a quantidade de recursos
utilizada por uma aplicação durante o acesso ao banco de dados, garantindo uma única
conexão. Internamente a classe mantém uma referência única para um objeto de tipo
Connection, criado pelo construtor privado declarado. Apenas através do método
“getConnection()” é possível obter a referência para a conexão única. Existe um método
adicional “shutdown()” criado para garantir que a conexão seja encerrada adequadamente.
Note que são arbitrárias as definições de campos e métodos adicionais dentro de uma classe
que pretende ser um Singleton, devendo atender aos requisitos específicos da aplicação.
Listagem 1. Implementação de um Singleton
package jandl.pattern.singleton;
import java.sql.*;
public final class DBConnection {
// referência para instância única
private static Connection instance = null;
// construtor privado
private DBConnection()
throws ClassNotFoundException, SQLException {
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
System.out.println("[Driver loaded]");
instance = DriverManager.getConnection("jdbc:odbc:Mamute");
System.out.println("[Connection done]");
}
// fornece acesso a instância única
public static Connection getConnection()
throws ClassNotFoundException, SQLException {
if (instance == null) {
// lazy instantiation: só quando preciso
new DBConnection();
}
System.out.println("[Connection obtained]");
return instance; // retorna instância única da conexão
}
// encerra conexão adequadamente
public static void shutdown()
throws ClassNotFoundException, SQLException {
if (instance != null) {
instance.close();
instance = null;
System.out.println("[Connection closed]");
}
}
}
Na Listagem 2 temos um exemplo simples que permite verificar o funcionamento do
Singleton. Através do método “getConnection()” da classe DBConnection obtemos uma
conexão para o banco de dados (cujas strings referentes ao driver e url de conexão foram
inclusas diretamente no código da classe). Através desta conexão podem ser estabelecidas
sessões com o banco de dados, o que permite a execução de comandos SQL e processamento
dos resultados obtidos. Tentativas de obtenção de novas conexões retornarão uma referência
para o mesmo objeto, reduzindo a quantidade de recursos tomados do sistema.
Listagem 2. Teste do Singleton
package jandl.pattern.singleton;
import java.sql.*;
public class DBConnectionTest {
public static void main(String a[]) throws Exception {
Connection con;
Statement stmt;
// obtém conexão
con = DBConnection.getConnection();
stmt = con.createStatement();
int r = stmt.executeUpdate( "INSERT INTO USERS VALUES('" + a[0] + "','"+ a[1] +"')" );
System.out.println(r + " row(s) affected");
stmt.close();
// obtém (a mesma) conexão
con = DBConnection.getConnection();
stmt = con.createStatement();
ResultSet rs = stmt.executeQuery( "SELECT * FROM USERS" );
while (rs.next()) {
System.out.println(rs.getString(1) + ":" + rs.getString(2));
}
rs.close();
stmt.close();
// encerra conexão
DBConnection.shutdown();
}
}
Usos conhecidos e padrões relacionados
A classe Runtime, da API do J2SE, possui um método “getRuntime()” que retorna uma
instância da própria classe, o qual permite o acesso ao ambiente de execução por parte da
aplicação. Como o ambiente é único, só pode existir uma instância desta classe, assim é claro
que Runtime implementa o padrão de projeto Singleton para cumprir com esta restrição.
Outros usos deste padrão estão associados ao acesso controlado de filas de impressão, portas
de comunicação e outros recursos restritos do sistema. Também é comum que este padrão
esteja associado a outros, tais como o Abstract Factory e o Facade.
O padrão Singleton pode ser modificado para que exista um número máximo de instâncias de
uma classe, caracterizando assim um pool de objetos, flexibilizando seu emprego nas
situações em que não é necessário garantir uma instância única, mas onde deve ser limitada a
quantidade total de objetos e recursos utilizados.
Factory Method
Motivação, propósito e aplicabilidade
Uma estratégia comum no desenvolvimento de sistemas é utilizar uma classe como base para
criação de várias outras subclasses mais especializadas. A base desta hierarquia é como uma
interface comum para os tipos derivados, intenção que pode ser reforçada tornando-a uma
classe abstrata ou uma interface. Assim novos tipos poderão ser adicionados à hierarquia sem
que seja afetado o código existente. Por outro lado, como prever a adição de novos tipos nos
trechos de código onde objetos deve ser criados, isto é, como escolher uma dentre as classes
disponíveis quando novas implementações são incorporadas ao sistema?
O padrão Factory Method (ou Método Fábrica) resolve este problema definindo uma interface
para criação de objetos (os "produtos") e deixando que uma outra classe (a "fábrica") decida
como será a instanciação de objetos. Isto permite isolar as requisições de novos objetos de sua
seleção e instanciação efetiva. Na implementação da classe "fábrica" deve existir um método
responsável pela instanciação dos "produtos", o método fábrica, que cria objetos pertencentes
a hierarquia de classes em questão, sendo portanto um outro padrão de criação. Podemos
empregar este padrão sempre que desejamos oferecer um serviço de criação de objetos quando
o conjunto de classes concretas deva ser modificado com a adição de novas classes. A única
exigência para isso é que as classes concretas sejam parte de uma mesma hierarquia,
possibilitando a manipulação de seus objetos pela interface definida em sua base.
Estrutura, participantes e conseqüências
Através da Figura 2 podemos observar que existem quatro componentes na estrutura deste
padrão. IProduct especifica a interface comum para os tipos especializados que deverão ser
instanciados. Uma ou mais classes ProductImpl, que são as classes concretas que
implementam a interface IProduct, dotando seus objetos de características próprias. A
interface IFactory, que determina a operação correspondente ao método fábrica propriamente
dito, por exemplo, “createProduct()”, que retorna objetos do tipo genérico IProduct. A classe
FactoryImpl implementa a interface de obtenção de objetos, ou seja, a operação
“createProduct()”, onde ocorre a seleção da classe concreta que será utilizada para a criação de
um objeto IProduct.
Figura 2. Estrutura do padrão de projeto Factory Method
O uso deste padrão separa a classe da aplicação das classes específicas, sendo que apenas no
método fábrica existem referências diretas às classes específicas. Outra conseqüência é que a
adição de novas subclasses de produto exigirá apenas a modificação do método fábrica,
tornando o código mais robusto e manutenível.
Implementação e exemplo
Inicialmente iremos nos concentrar nos “produtos” da fábrica, definindo a interface
IConverter, a qual será utilizada para a manipulação destes, como mostra a Listagem 3. Esta
interface define o método “convert(double)”, cujo propósito é permitir a conversão de um
valor numérico em outro segundo algum critério. Todos os “produtos” da fábrica deverão
implementar tal interface, ou seja, suprir o método “convert(double)”, determinando assim
uma conversão específica.
Listagem 3. Interface dos produtos
package jandl.pattern.factorymethod;
public interface IConverter {
public double convert (double value);
}
Classes destinadas à conversão, por exemplo, de temperaturas, de moedas ou unidades de
medida, poderiam implementar esta interface, tais como as exemplificadas na Listagem 4,
onde temos conversores de temperatura entre as escalas Celsius, Farenheit e Kelvin que
constituiriam possíveis “produtos” fornecidos pela “fábrica”.
Listagem 4. Implementação dos produtos
// Conversor Celsius to Farenheit
package jandl.pattern.factorymethod;
public class C2F implements IConverter {
public double convert(double value) {
return 9*value/5 + 32;
}
}
// Conversor Celsius to Kelvin
package jandl.pattern.factorymethod;
public class C2K implements IConverter {
public double convert(double value) {
return value + 273;
}
}
// Conversor Farenheit to Celsius
package jandl.pattern.factorymethod;
public class F2C implements IConverter {
public double convert(double value) {
return 5*(value - 32)/9;
}
}
// Conversor Farenheit to Kelvin
package jandl.pattern.factorymethod;
public class F2K implements IConverter {
public double convert(double value) {
return 5*(value - 32)/9 + 273;
}
}
// Conversor Kelvin to Celsius
package jandl.pattern.factorymethod;
public class K2C implements IConverter {
public double convert(double value) {
return value - 273;
}
}
// Conversor Kelvin to Farenheit
package jandl.pattern.factorymethod;
public class K2F implements IConverter {
public double convert(double value) {
return 9*(value - 273)/5 + 32;
}
}
Respeitando a proposta do padrão Factory Method, podemos definir uma interface que
caracterize o método “fábrica” propriamente dito. Na Listagem 5 temos uma sugestão para
isto, onde o único método especificado será “createConverter(int,int)”, utilizado para solicitar
a criação de novos “produtos” conforme os argumentos recebidos. O emprego de argumentos
no método “fábrica” constitui uma variante do padrão, que neste caso, representa as unidades
de entrada e saída dos conversores.
Listagem 5. Interface do método fábrica
package jandl.pattern.factorymethod;
public interface IConverterFactory {
public IConverter createConverter(int inS, int outS);
}
Uma “fábrica” de produtos pode ser criada implementando-se a interface IConverterFactory,
(Listagem 6, ConverterFactoryImpl), de modo que, conforme as unidades indicadas como
argumentos do método “createConverter(int,int)”, seja instanciado um conversor adequado.
Devemos ressaltar que apenas a classe ConverterFactoryImpl conhece as classes concretas que
constituem os “produtos”, pois os clientes da “fábrica” receberão instâncias de objetos que
serão genericamente usados através da interface de “produto” IConverter. Para tornar o uso
desta fábrica mais flexível, foram adicionadas constantes e strings correspondentes às
unidades suportadas. Caso não exista uma classe apropriada para um conversor das unidades
indicadas, o método “fábrica” retorna null, embora pudesse ter lançado uma exceção.
Listagem 6. Implementação da fábrica
package jandl.pattern.factorymethod;
public class ConverterFactoryImpl
implements IConverterFactory {
// constantes de escalas disponíveis
public static final int CELSIUS=0, FARENHEIT=1, KELVIN=2;
public static final String scales[] = {
"Celsius", "Farenheit", "Kelvin" };
// método fábrica
public IConverter createConverter(int in, int out) {
switch(in) {
case CELSIUS:
switch(out) {
case FARENHEIT: return new C2F();
case KELVIN: return new C2K();
}
break;
case FARENHEIT:
switch(out) {
case CELSIUS: return new F2C();
case KELVIN: return new F2K();
}
break;
case KELVIN:
switch(out) {
case CELSIUS: return new K2C();
case FARENHEIT: return new K2F();
}
break;
}
return null;
}
}
Na Listagem 7 temos uma aplicação de conversão de unidades de temperatura que utiliza a
“fábrica” para obter diferentes conversores, conforme a seleção de unidades. Observe que a
aplicação não conhece as classes específicas e seus detalhes, restringindo-se aos elementos
fornecidos pela “fábrica”. Alterações nas implementações dos conversores, bem como a
adição de novos conversores podem ser realizadas livremente, concentrando as modificações
correspondentes na “fábrica” que isola, portanto, a aplicação cliente das implementações dos
produtos.
Listagem 7. Aplicação da fábrica
package jandl.pattern.factorymethod;
import java.awt.*;
import java.awt.event.*;
import java.text.*;
import javax.swing.*;
public class ConverterUI extends JFrame {
private JComboBox chScIn, chScOut;
private JTextField tfValIn, tfValOut;
private ConverterFactoryImpl factory = new ConverterFactoryImpl();
private DecimalFormat df = new DecimalFormat("#####.00");
public ConverterUI() {
super("ConverterUI");
Container c = getContentPane();
c.setLayout(new GridLayout(3, 3, 2, 2));
// adição dos componentes no ContentPane
c.add(new JLabel());
c.add(new JLabel("In"));
c.add(new JLabel("Out"));
c.add(new JLabel("Scale"));
c.add(chScIn = new JComboBox(factory.scales));
c.add(chScOut = new JComboBox(factory.scales));
c.add(new JLabel("Value"));
c.add(tfValIn = new JTextField());
c.add(tfValOut = new JTextField());
// listeners e ajustes nos componentes
tfValIn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
doConvertion();
}
});
tfValOut.setEditable(false);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
pack();
setResizable(false);
}
private void doConvertion() {
IConverter conv = factory.createConverter(
chScIn.getSelectedIndex(), chScOut.getSelectedIndex());
try {
double value = df.parse(tfValIn.getText()).doubleValue();
if (conv!=null) {
value = conv.convert(value);
}
tfValOut.setText(df.format(value));
} catch (Exception e) {
tfValIn.selectAll();
tfValIn.requestFocus();
Toolkit.getDefaultToolkit().beep();
tfValOut.setText("");
}
}
public static void main(String a[]) {
new ConverterUI().setVisible(true);
}
}
É comum o emprego do padrão Factory Method combinado com outros padrões, tais como o
Singleton, para garantir uma instância única da “fábrica”, ou Abstract Factory, que generaliza
ainda mais o processo de criação de objetos. Também é possível que “fábricas” forneçam
elementos que implementem outros padrões, como Iterator ou Decorator. Existem muitos
exemplos de utilização deste padrão na API do J2SE, tais como as classes
javax.swing.BorderFactory, java.text.Collator e java.net.SocketFactory.
Facade
Motivação, propósito e aplicabilidade
Existem circunstâncias onde é necessário utilizar diversas classes diferentes para que uma
tarefa possa ser completada, caracterizando uma situação onde uma classe cliente necessita
utilizar objetos de um conjunto específico de classes utilitárias que, em conjunto, compõem
um subsistema particular ou que representam o acesso a diversos subsistemas distintos. O uso
deste conjunto variado de classes introduz naturalmente uma complexidade maior na tarefa
executada pela classe cliente, pois cada classe utilitária tem uma interface própria,
dificultando sua organização e codificação. O padrão de projeto denominado Facade,
conhecido também como Fachada, pretende reduzir a complexidade do relacionamento entre a
classe cliente e as demais classes utilitárias, fornecendo uma interface única e simplificada
para o acesso às interfaces das classes utilitárias. O Facade é um padrão de projeto
considerado como estrutural, pois sua responsabilidade consiste na organização de uma
interface mais simples para permitir o uso de múltiplas outras interfaces, constituindo uma
abstração de alto nível. Este padrão é aplicável quando se deseja uma interface mais simples
para um ou mais subsistemas complexos, sendo que esta nova interface é uma espécie de
visão particularizada dos subsistemas que encapsula. Também pode ser empregado para
reduzir as dependências entre o cliente e as classes existentes no subsistema, possibilitando
isolar a classe do cliente das classes de implementação do subsistema, reduzindo assim a
coesão do sistema.
Estrutura, participantes e conseqüências
A estrutura deste padrão é bastante simples, como mostra a Figura 3. A classe que constituirá
o Facade deverá oferecer um conjunto de operações que sejam suficientes para que seus
clientes possam utilizar adequadamente o subsistema, embora sem conhecer as interfaces de
seus componentes. As classes dos subsistemas devem implementar as funcionalidades
específicas necessárias, realizando as tarefas do cliente por meio da delegação da classe
Facade. Isto significa que os clientes não precisam acessar diretamente as classes do
subsistema, nem necessitam conhecê-las. Desta forma, modificações realizadas nas classes do
subsistema, incluindo-se nisto adição de novas classes e funcionalidades, poderão passar
despercebidas pelo cliente, desde que a interface do Facade seja mantida.
Figura 3. Estrutura do padrão de projeto Facade
Implementação e exemplo
Implementar um Facade demanda definir um conjunto de operações reduzido, que permita
ocultar a complexidade inerente à utilização de várias classes de um subsistema. Tomemos a
questão do acesso a banco de dados utilizando as classes mais comuns do JDBC. Para que
uma única operação de consulta seja realizada, é necessário utilizarmos diversos objetos:
através da classe DriverManager obtemos um objeto Connection; com este obtemos um objeto
Statement; com o qual realizamos a consulta, fornecendo-nos um objeto ResultSet para que,
finalmente, tenhamos acesso aos dados retornados. Um Facade simples poderia encapsular a
manipulação destes objetos, retornando apenas um objeto independente como resposta a uma
query (um consulta SQL realizada por meio de uma operação SELECT), como mostra a
Listagem 8.
Listagem 8. Implementação de Facade
package jandl.pattern.facade;
import jandl.pattern.singleton.*;
import java.sql.*;
import java.util.*;
public class QueryFacade {
private Connection con;
// construtor
public QueryFacade() {
try {
// usamos Singleton de conexão ao BD
con = DBConnection.getConnection();
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
}
// método que encapsula consulta
public List executeQuery(String query) {
try {
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(query);
ResultSetMetaData rsmd = rs.getMetaData();
int colsCount = rsmd.getColumnCount();
List result = new LinkedList();
while (rs.next()) {
String row[] = new String[colsCount];
for(int i=0, j=1; i<colsCount; i++, j++) {
row[i] = rs.getString(j);
}
result.add(row);
}
rs.close();
stmt.close();
return result;
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
}
// método que encapsula shutdown da conexão ao BD
public void shutdown() {
try {
DBConnection.shutdown();
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
}
}
O construtor de QueryFacade obtém uma conexão ao banco de dados através do Singleton de
conexão (Listagem 1), evitando múltiplas conexões. Através do método
“executeQuery(String)” o cliente pode apenas indicar a consulta que deseja realizar,
recebendo com resultado uma coleção do tipo List, cujos elementos são arrays de String,
contendo cada um os dados correspondentes aos registro obtido na consulta. Estes elementos
podem ser acessados através de um objeto Iterator (que na API J2SE representa a
implementação do padrão de mesmo nome e que permite “navegar” pelos elementos de uma
estrutura de dados). Não foi retornado o objeto ResultSet, pois o mesmo está vinculado ao
objeto Statement utilizado, além disso esta estratégia isola completamente o cliente das
classes JDBC utilizadas na consulta. Na Listagem 9 temos uma aplicação de console que testa
o Facade construído, onde podemos notar que a realização da consulta tornou-se mais simples
em relação à forma convencional (usada na Listagem 2). O processamento dos dados toma o
restante do código, mas é feito sem conhecimento específico da coleção retornada (sabemos
apenas que cada elemento é um array de String contendo os dados de cada registro,
informação essa associada ao Facade em si).
Listagem 9. Aplicação do Facade
package jandl.pattern.facade;
import java.util.*;
public class QueryFacadeTest {
public static void main(String a[]) {
// instanciação e uso do Facade
QueryFacade qf = new QueryFacade();
List result = qf.executeQuery("SELECT * FROM USERS");
// processamento dos dados
Iterator i = result.iterator();
StringBuffer data = new StringBuffer();
int c;
while (i.hasNext()) {
String row[] = (String[])i.next();
for(c=0; c<row.length-1; c++) {
data.append(row[c]);
data.append(",");
}
data.append(row[c]);
data.append("\n");
}
// exibição e finalização
System.out.println(data.toString());
qf.shutdown();
}
}
Notamos que o emprego de um Facade pode reduzir significativamente a complexidade do
código utilizado para a realização de tarefas associadas a vários subsistemas ou múltiplas
classes. O segredo deste padrão está no projeto de uma interface flexível para o Facade. Na
API J2SE, a classe java.net.URL utiliza este padrão em sua implementação.
Para Saber Mais
A referência máxima no assunto ainda é o livro “Padrões de Projeto”, de E. Gamma; R. Helm;
R. Johnson; e J. Vlissides (Bookman, 2000); mas os exemplos são apresentados em dialeto
C++. Vários livros sobre Java incluem capítulos sobre padrões de projeto, entre eles “Mais
Java”, de Peter Jandl Jr. (Futura, 2003); e “Sun Certified Enterprise Architect for J2EE: guia
oficial de certificação”, de P. Allen e J. Bambara (Campus, 2003). Existem também muitos
links interessantes, entre eles:
The Hillside Group – Patterns Homepage
http://hillside.net/patterns/
Appleton, B. Patterns and Software: Essential Concepts and Terminology
http://www.cmcrossroads.com/bradapp/docs/patterns-intro.html
Lea, D. Patterns-Discussion FAQ
http://g.oswego.edu/dl/pd-FAQ/pd-FAQ.html
Considerações Finais
Através dos exemplos vistos, pudemos observar conceitualmente que o emprego dos padrões
de projeto pode melhorar de maneira substancial a arquitetura de uma solução de software. No
início é comum encararmos com certo ceticismo a quantidade extra de classes introduzidas
pelos padrões, mas tal esforço vale cada linha de código construída, pois tais soluções
permitem isolar as partes de um projeto, tornando-as independentes e mais flexíveis. O
resultado é um sistema mais robusto e mais simples de ser mantido e ampliado. Além disso os
padrões representam um vocabulário de alto nível para a troca de experiências e conhecimento
relacionados ao projeto de software. Embora não sejam uma solução milagrosa, nem aplicável
à todas as situações, depois de estudados, os padrões de projeto poderão ser reconhecidos,
combinados e aplicados com relativa e crescente facilidade, sem contar com a possível
descoberta de novos padrões associados aos problemas abordados. Embora não tenhamos
realizado aqui um estudo exaustivo dos padrões de projeto, fica o convite para um
aprofundamento no tema, pois eles são definitivamente importantes para o projeto de sistemas
e também reforçam algo que já sabíamos: o quanto o Java é moderno e adequado para o
desenvolvimento de software orientado a objetos.
Referências
[1] BECK, Kent, JOHNSON, Ralph. "Patterns Generate Architectures" IN Proceedings of ECOOP'94 - European
Conference on Object Oriented Programming, pp. 139-49, Bologna, Italy, July/1994, Springer-Verlag.
[2] COPLIEN, James O. "Software Patterns". New York, NY (USA): SIGS Books, 1996.
[3] GAMMA, Erich, HELM, Richard, JOHNSON, Ralph, VLISSIDES, John. "Padrões de Projeto: Soluções reutilizáveis
de software orientado a objetos". Porto Alegre, RS: Bookman, 2000.
[4] JANDL, Peter Jr.. “Mais Java”. São Paulo, SP: Futura, 2003.
[5] PAGE-JONES, Meilir. "Fundamentos do Desenho Orientado a Objetos com UML". São Paulo, SP: Makron Books,
2001.
Autor
“Peter Jandl Jr.” ([email protected]) é formado em engenharia elétrica pela Unicamp e mestre em educação
pela USF. Leciona no ensino superior há 16 anos em cursos de engenharia da computação, ciência da
computação e análise de sistemas. Programador certificado Java pela Sun, é autor dos livros “Introdução ao
Java” e “Mais Java”. Hoje é professor da Universidade São Francisco e coordenador do curso de Ciência da
Computação da Faculdade de Jaguariúna.
Download