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.