• Nenhum resultado encontrado

PADRÕES DE PROJETO. capítulo seis

N/A
N/A
Protected

Academic year: 2021

Share "PADRÕES DE PROJETO. capítulo seis"

Copied!
69
0
0

Texto

(1)

PADRÕES DE PROJETO

Desenvolver software é considerado uma tarefa árdua. E não faltam razões para essa afirmação. Se considerarmos que na medida em que os problemas crescem em complexidade, aumenta também o número de soluções possíveis, temos aí um grande desafio: qual, dentre as soluções que

vislumbramos, devemos escolher? A busca pela melhor solução, antes mesmo de efetivamente termos resolvido o problema, usualmente traz mais dificuldades ao processo de desenvolvimento. Knuth1 (1968) simplesmente diria que “a otimização prematura é a raiz de todo mal”. Como muitos dos projetistas de tecnologia e desenvolvedores parecem limitar o conjunto de alternativas visíveis a uma estreita faixa de soluções esboçadas pela comunidade técnica onde se inserem (Jensen & La Porte, 1996), simplesmente muitas tecnologias novas e soluções deixam de surgir.

Na maioria dos casos, notamos que o entendimento do problema não é absolutamente claro: é comum que a documentação esteja incompleta, e às vezes ausente, quanto ao problema em si e à solução encontrada para ele. Dessa maneira, problemas idênticos, que se repetem em outros contextos, não são reconhecidos como tal, consumindo tempo e recursos para a implementação de soluções que já haviam sido encontradas.

Atualmente o desenvolvimento de software faz uso intenso da orientação a objetos, que permite, em tese, agregar aos sistemas assim desenvolvidos as seguintes qualidades: confiabilidade, robustez, distributividade, armazenabilidade, extensibilidade e reusabilidade (Page-Jones, 2001, p. 68). Mas o uso da orientação a objetos, por si só, não garante a obtenção dessas qualidades e parece depender muito mais das técnicas usadas nas etapas de análise e projeto desses sistemas do que realmente das linguagens e ferramentas empregadas no desenvolvimento e teste, evidenciando as questões relacionadas à compreensão do problema e sua documentação.

Os padrões de projeto, também conhecidos como design patterns, vem despertando o interesse da comunidade de projetistas de software por permitir não apenas a documentação de problemas e suas soluções, mas por proporcionar elementos que conduzem ao reaproveitamento do projeto dessas soluções, possibilitando que os sistemas construídos sob essa orientação exibam as qualidades desejadas.

1. Donald E. Knuth é um dos maiores cientistas da computação de que se tem notícia, tendo colaborado em diversas áreas, entre elas, programação, algoritmos e sistemas operacionais.

(2)

406

Como motivação adicional, toda a plataforma Java faz uso intensivo dos padrões de projeto, ou seja, depois de estudados os padrões, reconheceremos com facilidade onde foram aplicados nas diversas classes da API, reforçando algo que já sabíamos: o quanto o Java é moderno e adequado para o desenvolvimento de software orientado a objetos.

6.1 O que são os padrões de projeto?

Existem várias definições para os padrões de projeto. Tomemos uma dessas definições, hoje considerada clássica, proveniente do grupo conhecido como GoF (Gang of Four):

“Um padrão de projeto sistematicamente nomeia, motiva e explica um projeto genérico, que endereça um problema de projeto recorrente em sistemas orientados a objetos. Ele descreve o problema, a solução, quando é aplicável e quais as conseqüências de seu uso.” [Tradução livre] (Gamma, Helm, Johnson & Vlissides, 1995, p. 360)

Nessa definição, vemos enfatizadas as questões do entendimento do problema e da solução, da ocorrência do problema repetidas vezes e de sua aplicabilidade, motivando o projeto na direção de uma solução genérica. Cada padrão de projeto é então o responsável por um tipo de problema, que pode ocorrer repetidas vezes em nosso ambiente, e pela sua solução, de modo que possamos reutilizá-lo. Dessa maneira, certas soluções, que foram utilizadas em situações particulares, podem ser novamente usadas por outros desenvolvedores em situações semelhantes. A visão de Coplien do conceito de padrão de projeto é similar:

“Um padrão (de projeto) é uma peça de literatura que descreve um problema de projeto e a solução geral para o problema em um contexto particular”. [Tradução livre] (Coplien, 1996, p. 2)

Embora existam várias formas propostas para a descrição de padrões de projetos, todas exigem que os problemas e as soluções envolvidos sejam documentados de maneira estruturada, criando uma cultura de catalogação de experiências e soluções que auxiliem o desenvolvimento de novos softwares. Vejamos uma outra definição:

“Padrões de projeto capturam a experiência na construção de software orientado a objetos”. [Tradução livre] (Budinsky, Finnie, Vlissides & Yu, 1996)

Sendo o desenvolvimento de software uma tarefa reconhecidamente difícil, a experiência (enquanto essência das habilidades desenvolvidas ao longo do tempo em uma área específica) torna-se inestimável. Idealmente gostaríamos que os projetistas mais experientes pudessem

(3)

407

compartilhar tal conhecimento com os ‘novatos’, não apenas acelerando seu amadurecimento profissional, mas ampliando as chances do surgimento de soluções inovadoras, de novas técnicas e metodologias de trabalho. Mas tal ‘transferência’ de experiências é outro desafio existente na comunidade dos projetistas e desenvolvedores de software.

Uma tática usada pelos projetistas é evitar a construção de soluções do início ao fim, isto é, entendido o problema, devem ser identificadas as partes semelhantes a sistemas já existentes e proceder-se sua reutilização. Também é comum que, a cada ciclo de uso de uma solução, sejam incorporadas novas características que aperfeiçoem ou generalizem a solução encontrada. Mas isso só pode ser feito por projetistas experientes, que já tenham passado por tais experiências, e desde que os problemas e soluções sejam documentados (o que não é uma tarefa considerada

agradável).

Como muitas das metodologias de projeto acabam por focar excessivamente a solução em si, descrevendo muito precisamente ‘o que’ e ‘como’ deve ser feito, deixando de lado o ‘por que’ da solução (Kent & Johnson, 1994, p. 139), a documentação produzida pode não ser efetivamente útil. Não se trata de desvalorizar a importância das técnicas de projeto e da documentação das

soluções, mas o entendimento do problema é a chave das questões aqui apresentadas em relação às dificuldades do desenvolvimento e também do compartilhamento do conhecimento adquirido com as soluções encontradas. Mais importante que a solução em si é a clara descrição do problema e da forma com que uma solução se torna aplicável, incluindo-se aí as limitações e as conseqüências do emprego dessa solução.

Os padrões de projeto pretendem preencher essas lacunas, tornando-se um mecanismo eficiente para a comunicação e o compartilhamento de conhecimento entre os desenvolvedores. A idéia é a criação de uma linguagem comum, capaz de exprimir mais do que simples estruturas de dados, módulos, rotinas ou objetos, mas a articulação desses elementos em soluções arquitetônicas para o software. Ao mesmo tempo, os padrões de projeto não se prendem a linguagens de

programação ou contextos particulares, permitindo a catalogação de soluções testadas e aprovadas permitindo sua reutilização. Os padrões de projeto devem ser capazes de exibir a racionalidade existente em uma proposta de solução (Budinsky, Finnie, Vlissides & Yu, 1996) compondo um vocabulário para discussão e aperfeiçoamento das questões relacionadas ao desenvolvimento de software orientado a objetos. Ao citar-se um padrão de projeto, todo um conjunto de conceitos, relativo a um problema e sua solução, são implicitamente associados, permitindo a discussão em um elevado nível de abstração.

Sendo assim, os padrões de projeto tornam-se um mecanismo fundamental para a expressão de projetos de software e, portanto, do entendimento amplo de certos problemas de projeto. Exibem como principais características a possibilidade de reutilização dos projetos em si (e não apenas do código usado em suas implementações) e a definição de uma linguagem de análise e discussão de problemas e soluções de projeto, proporcionando economia de tempo, compartilhamento efetivo de experiências e construção de sistemas melhores.

(4)

408

Ainda citando GoF:

“Padrões de projeto identificam, denominam e abstraem temas comuns do projeto

orientado a objetos. Eles preservam a informação do projeto capturando a intenção anterior ao projeto. Eles identificam classes, instâncias, seus papéis, colaborações e a distribuição das responsabilidades”. [Tradução livre] (Gamma, Helm, Johnson & Vlissides, 1993, p. 408)

Nesse sentido, os padrões de projeto são modelos de soluções relacionados a problemas

específicos, mas que ocorrem em inúmeros e diferentes contextos. Por meio deles, o entendimento das questões envolvidas é mais amplo, as possibilidades de aplicação bem como os compromissos e limitações existentes são melhores caracterizados; portanto, à medida que um projetista se familiariza com diferentes padrões de projeto, é possível que o mesmo adquira uma parcela da experiência daqueles que desenvolveram e documentaram tais padrões. O emprego de tais padrões aos projetos evita que as soluções existentes, já testadas e aprovadas, venham a ser reinventadas, permitindo a concentração de esforços nos aspectos inéditos do problema e tornando os sistemas assim desenvolvidos provavelmente mais robustos e confiáveis.

6.2 Um breve histórico

As idéias que deram origem aos padrões de projeto ocorreram em um contexto muito diferente da área de engenharia de software. Em 1977, o arquiteto Christopher Alexander e seus colegas publicaram o livro “A pattern language”, uma espécie de catálogo com 253 padrões construtivos, defendendo que os métodos tradicionais empregados pela arquitetura não supriam as reais necessidades dos usuários das construções propostas e, portanto, que a arquitetura não atendia a sociedade como deveria, pois seu maior objetivo é melhorar a qualidade de vida das pessoas. Cada um dos padrões apresentados era como um pequeno manual sobre uma questão arquitetônica comum (Alexander et al., 1977, Lea, 1994), onde o conceito central de padrão é:

“Cada padrão descreve um problema que ocorre outra e mais outra vez no nosso ambiente, e então descreve o âmago da solução desse problema, de forma que você possa usar essa solução um milhão de vezes, sem fazer o mesmo duas vezes”. [Tradução livre] (Alexander et al., 1977)

Em 1979, Alexander publica “A timeless way of building”, onde formaliza método e o elemento racional de suas idéias publicadas anteriormente, descrevendo um padrão como:

“Cada padrão é uma regra com três partes, que expressa uma relação entre um certo contexto, um problema e uma solução”. [Tradução livre] (Alexander, 1979, p. 247)

(5)

409

Nesses livros, Alexander defendia que o uso de padrões não limitaria os arquitetos às soluções prescritas, mas garantiria a presença dos elementos fundamentais que levam ao conforto, articulando função e forma nas construções e nos ambientes ali existentes, integrando-os à natureza, pois tais padrões exprimiam conceitos atemporais de qualidade.

Vários anos depois, Ward Cunnigham e Kent Beck, trabalhando em um projeto envolvendo desenvolvimento de interfaces de usuário e inspirados pelas idéias de Alexander, propõem cinco padrões, implementados em Smalltalk2, que proporcionaram grandes ganhos ao projeto. Esses padrões e algumas conclusões foram apresentados em 1987 na OOPSLA (Object-Oriented Programming Systems, Languages and Applications). Erich Gamma, em um trabalho paralelo durante sua tese de doutorado, nota a importância do uso de padrões repetitivos em projetos. Em uma conferência integrada da OOPSLA e ECOOP (European Conference on Object-Oriented Programming), em 1990, Gamma, em conjunto com Bruce Anderson e Richard Helm, discute sobre padrões.

Em uma série de conferências sobre orientação a objetos que se sucederam, o tema começou a ganhar mais espaço e mais adeptos, entre eles: James Coplien, Ralph Johnson, John Vlissides, Desmond D’Souza, Douglas Lea, Norm Kerth e Wolfgang Pree. Livros e inúmeros artigos começam a ser publicados divulgando o assunto, tal como “Advanced C++ programming styles and idioms” (Coplien, 1992).

Após um workshop patrocinado pela IBM, em maio de 1993, no qual muitos dos pesquisadores citados se encontraram, Kent Beck e Grady Booch organizaram um retiro no Colorado (Estados Unidos), reunindo um grande número de estudiosos. A partir desse evento, esse grupo começou a manter contato freqüente e a autodenominar-se “The hillside group”, hoje uma instituição

independente dedicada ao estudo dos padrões de projeto. Uma conferência específica, a PLoP (Pattern Languages of Programming) é organizada por membros do grupo em 1994, repetindo-se várias outras vezes nos anos seguintes. Logo após a “Gang of four”, publica seu compêndio sobre padrões: “Design patterns: elements of reusable object-oriented software” (Gamma, Helm, Johnson & Vlissides, 1995), uma referência absoluta no tema (embora infelizmente só contenha exemplos em C++ e Smalltalk).

Um outro grupo, às vezes referenciado como “Gang of five”, composto por Frank Buschmann, Regine Meunier, Hans Rohnert, Peter Sommerlad e Michael Stal, publicou o livro “Pattern-oriented software architecture”, outra importante e conhecida referência.

A partir daí o tema ‘design patterns’, ou seja, os padrões de projeto, tornam-se uma coqueluche, pois trouxe uma proposta concreta para expressar não apenas estruturas de projetos, mas retratar a essência de soluções de problemas comuns, representando um novo e rico vocabulário para troca de experiências e discussões sobre engenharia de software e desenvolvimento de sistemas orientados a objetos.

2. Linguagem e ambiente de programação orientada a objeto, que surgiu por volta de 1970 e é considerada por muitos como uma implementação pura da orientação a objetos.

(6)

410

6.3 Características dos padrões de projeto

Os padrões de projeto possuem algumas características importantes que devem ser destacadas (Lea, 1994, Gamma et al., 1995, Coplien, 1996):

±

Os padrões de projeto devem descrever e justificar as soluções para problemas concretos e bem definidos, isto é, não são estratégias ou princípios de implementação.

±

As soluções descritas devem estar comprovadas, isto é, devem ter sido previamente experimentadas e testadas.

±

O problema tratado por um padrão 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.

±

Usualmente os padrões de projeto descrevem relações entre conceitos, mecanismos e estruturas existentes nos sistemas.

±

Os padrões devem ser capazes de capturar a evolução e o aprimoramento das soluções, assim como equilibrar os pontos fortes e fracos encontrados, sendo assim não são soluções óbvias ou triviais.

±

Os padrões devem ser utilizados em conjunto com outros padrões, compondo linguagens de padrões (pattern languages).

Assim, podemos afirmar que o conceito de ‘padrão de projeto’ é mais amplo e talvez mais adequado que o de ‘classe’, empregado na orientação a objetos, pois permite exprimir adequadamente conceitos e estruturas existentes que não são necessariamente objetos.

Finalmente, os padrões devem ser úteis, no sentido de reunir as experiências em torno da solução de um problema, e utilizáveis, oferecendo elementos que possibilitem seu emprego efetivo.

6.4 Descrevendo padrões de projeto

Para que os padrões possam ser usados como um vocabulário específico para projetistas de software e depois catalogados como dicionários de soluções, devem ser descritos adequadamente. Como não são implementações, mas soluções de alto nível, são descritos textualmente, de modo a caracterizar o problema, o contexto onde este ocorre, sua análise e a solução proposta (Coplien, 1996, p. 7).

Considerando que descrições textuais podem ser estruturadas de inúmeras maneiras, é interessante a utilização de um roteiro que oriente quais elementos devem ser documentados, facilitando sua utilização e entendimento. Existem diversas formas, entre elas algumas mais populares: Alexander (Alexander, 1977, p. X-XI), GoF (Gamma, et al., 1995, p. 6-7), Portland (Cunningham, 2002) ou Coplien (Coplien, 1996, p. 14). Essencialmente essas formas incluem nome do padrão de projeto, propósito, problema, contexto, dificuldades, limitações ou restrições, solução, resultados, esboços e exemplos.

(7)

411

A forma GoF é muito objetiva, organizada e dirigida à produção de software orientada a objetos, sendo muito adequada para a elaboração de um catálogo de padrões. Ela tem a seguinte estrutura:

±

Nome e classificação

O nome de um padrão é um importante elemento, pois desejamos que seja adotado pelos desenvolvedores como uma referência que descreve um problema de projeto, sua solução, características e conseqüências. Não é uma tarefa fácil encontrar um nome representativo e ao mesmo tempo simples. A classificação é relacionada aos tipos de padrão, na visão do GoF, e pode ser considerada opcional.

±

Propósito

Descrição sintética do propósito do padrão, o que ele faz, seu racional e que problema de projeto pretende solucionar.

±

Nome secundário

Se existir, quais são os outros nomes que poderiam identificar esse padrão.

±

Motivação

Descreve o problema e o contexto onde ocorre, de maneira mais detalhada, esquematizando como os elementos usados pelo padrão permitem solucionar o problema.

±

Aplicabilidade

Caracteriza as situações onde o padrão pode ser aplicado, possivelmente citando um caso de projeto problemático no qual o emprego desse padrão seria recomendado. É

interessante relacionar como identificar essas situações de uso.

±

Estrutura

Representação gráfica da estrutura das classes envolvidas no padrão utilizando uma notação padronizada3.

±

Participantes

Especifica os elementos (classes e objetos) que compõem a solução e suas

responsabilidades. Não descreve uma implementação particular, mas as idéias contidas na solução.

±

Colaborações

Descreve como os participantes do padrão se relacionam, ou seja, como desempenham seus papéis na estrutura da solução.

±

Conseqüências

Relaciona os resultados do emprego desse padrão, as vantagens e desvantagens, envolvendo questões como flexibilidade, portabilidade e outras.

3. Originalmente utilizava a notação OMT — Object Modeling Technique (Rumbaugh et al., 1991). Hoje seria mais adequado o uso da UML — Unified Modeling Language (Coad et al., 1999, Page-Jones, 2001).

(8)

412

±

Implementação

Relaciona técnicas úteis na implementação do padrão, assim como conselhos e avisos sobre problemas potenciais, bem como questões relacionadas ao uso de linguagens específicas.

±

Exemplo

Trechos de código em linguagens representativas (C++ ou Java) que demonstrem a implementação do padrão.

±

Usos conhecidos

Situações reais de uso do padrão.

±

Padrões relacionados

Padrões de projeto que fazem parte desse padrão ou poderiam (ou não) ser usados para solucionar o problema de projeto em análise.

6.5 Classificação dos padrões

Além dos padrões de projeto existem outros, cujo âmbito pode ser mais ou menos restrito, embora tal classificação seja um tema muito subjetivo. Alguns autores (Buschmann & Meunier, 1995; Coplien, 1996) organizam os padrões em uma hierarquia de três níveis, como ilustrado na Figura 6.1, que são:

±

Padrão de arquitetura (Architectural Patterns ou Framework Patterns)

Oferecem um esquema de organização estrutural de sistemas que fornece um conjunto de subsistemas predefinidos, com responsabilidades específicas, incluindo regras para organizar os relacionamentos entre eles. Se preocupam com o sistema como um todo, ou seja, seus componentes e propriedades globais. Representam o nível mais alto dos padrões.

±

Padrões de projeto (Design Patterns)

Descrevem problemas que se repetem, o contexto específico de sua ocorrência e a solução em termos estruturais. Um padrão de projeto abstrai toda uma situação e sua solução, podendo ser considerado uma microarquitetura, cuja aplicação não afeta a estrutura global do sistema no qual se emprega. Também provê um esquema de refinamento tanto dos subsistemas e componentes do software, como dos relacionamentos entre eles. Representam o nível intermediário dos padrões.

±

Idioma (Idioms)

Um idioma é um padrão de projeto implementado em uma determinada linguagem de programação, que considera aspectos específicos dos seus componentes (classes e objetos) e de suas relações. Dado sua especificidade, é considerado um padrão de baixo nível.

(9)

413

Figura 6.1 Níveis dos padrões de projeto.

Os padrões de projeto, especificamente falando, poderiam ser divididos em três tipos ou categorias (Gamma et al., 1993, 1995) que indicam a natureza essencial dos padrões:

±

Padrões de criação (creational patterns), relacionados com o processo de criação de objetos (instanciação).

±

Padrões estruturais (structural patterns), que consideram formas de composição de classes ou objetos.

±

Padrões de comportamento (behavior patterns), que tratam a maneira com que classes e objetos podem interagir.

Essa divisão dos padrões objetiva apenas elucidar a característica principal da atuação de um certo padrão, se associando ao próprio padrão para compor um vocabulário para discussão e

desenvolvimento de projetos de software.

6.6 Catálogo de padrões

Nesta seção, apresentaremos um conjunto de padrões de projeto úteis, onde discutiremos o problema e a solução proposta, incluindo pequenas implementações que ilustram sua aplicação. Adotaremos uma variação resumida da forma GoF. Também destacaremos onde esses padrões são empregados na API Java. Os padrões que serão tratados e seus respetivos tipos são:

Criação Estruturais Comportamento

Singleton (6.6.1) Composite (6.6.4) Iterator (6.6.2) Factory Method (6.6.3) Decorator (6.6.6) Command (6.6.5) AbstractFactory (6.6.9) Adapter (6.6.8) Strategy (6.6.7)

Observer (6.6.10)

(10)

414

6.6.1 Singleton

Motivação, propósito e aplicabilidade

Motivação, propósito e aplicabilidade

Motivação, propósito e aplicabilidade

Motivação, propósito e aplicabilidade

Motivação, propósito e aplicabilidade

É um padrão de projeto muito simples, que permite ilustrar muitas das características necessárias aos padrões de projeto, possui inúmeras aplicações e sua implementação é muito direta. Segundo a classificação GoF, é um padrão de criação.

A motivação para esse padrão vem do fato de que muitas aplicações necessitam garantir a existência de uma única instância de certas classes, pois tais objetos podem fazer uso de recursos cuja utilização deve ser exclusiva, ou porque desejamos que os demais elementos do sistema compartilhem de um único objeto particular. Tais situações surgem quando vários subsistemas utilizam um único arquivo de configuração, permitindo sua modificação concorrentemente. Outra situação possível é quando uma conexão com um banco de dados ou um sistema remoto só pode ser estabelecida de modo exclusivo, tendo de ser compartilhada por vários módulos existentes. Em sistemas dotados de múltiplas janelas de tipos distintos, é comum que algumas dessas janelas não necessitem ou não possam ser instanciadas inúmeras vezes.

Como não queremos que os usuários dessas classes tenham que zelar por essa condição, ao mesmo tempo que desejamos prover acesso simples à única instância que poderá existir, adicionaremos essas responsabilidades às classes cujo comportamento será o de um Singleton. Assim sendo, o padrão de projeto Singleton tem como propósito garantir que para uma classe específica só exista uma dada instância, a qual possa ser obtida de modo global e uniforme.

Estrutura, participantes e conseqüências

Estrutura, participantes e conseqüências

Estrutura, participantes e conseqüências

Estrutura, participantes e conseqüências

Estrutura, participantes e conseqüências

A estrutura do Singleton está ilustrada no diagrama de classes da Figura 6.2.

Figura 6.2 Estrutura do padrão Singleton.

A implementação desse padrão de projeto solicita que a classe, para a qual só deve possuir uma instância, exiba as características da estrutura ilustrada, ou seja:

±

Classe Singleton

Efetua o armazenamento da única instância permitida;

Implementa a operação getInstance(), a qual garante que apenas um objeto será criado, retornando tal instância.

(11)

415

Instâncias das classes que implementam o padrão Singleton só poderão ser obtidas por meio da operação pública e estática getInstance() que retorna a instância única.

Implementação e exemplo

Implementação e exemplo

Implementação e exemplo

Implementação e exemplo

Implementação e exemplo

Uma implementação Java do padrão Singleton, como no Exemplo 6.1, deve utilizar-se de campo estático do tipo da própria classe para armazenar a referência da única instância permitida. Um método estático, também com tipo de retorno da própria classe, provê um 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 operação de criação só ocorrerá quando for estritamente necessário (lazy instantiation).

Como o Java suporta a clonagem de objetos (Sun, 2001A), devemos fazer tal classe final, ou seja, devemos impedir que essa classe seja utilizada como superclasse na criação de subclasses por meio da herança, pois se as subclasses implementarem a interface java.lang.Cloneable, objetos assim obtidos poderão ser duplicados (por meio do método clone()), o que romperia com as obrigações desse padrão.

Esse padrão pode ser modificado para permitir que um número maior de instâncias seja criado, limitados a um valor determinado, caracterizando um pool de objetos.

Temos no Exemplo 6.1 um modelo de implementação Java de um Singleton com a classe SingletonImp. Esse exemplo não oferece outras funcionalidades a não ser aquelas descritas pelo padrão de projeto Singleton, mas servindo apenas como uma implementação de referência. Uma classe concreta que necessite ter apenas um único objeto instanciado pode adicionar os métodos e campos necessários à estrutura sugerida.

// SingletonImpl.java

// classe declarada como final // impede seu uso através de herança public final class SingletonImpl {

// campo estático armazena a única // instância desta classe

private static SingletonImpl instance = null; // outros campos podem ser adicionados // construtores devem ser privados // para impedir uso externo private SingletonImpl() {

// campos da classe podem ser // normalmente inicializados }

// retorna o único objeto que // pode ser instanciado

public static SingletonImpl getInstance() { if (instance==null) {

// instanciação ocorre apenas se um // objeto for solicitado mas só uma vez

(12)

416

instance = new SingletonImpl(); }return instance;

}}

Exemplo 6.1 Modelo para implementação de Singleton.

Um outro exemplo de implementação do padrão Singleton é exibido no Exemplo 6.2, onde a classe SingleDBConn garante que apenas uma conexão será realizada com o banco de dados cuja URL é obtida de um arquivo de configuração (um arquivo de propriedades) denominado

‘config.properties’, garantindo que essa classe possa ser usada em diferentes situações sem necessidade de recompilação. Em vez de ser retornada uma instância do tipo SingleDBConn por meio de um método getInstance(), optamos por retornar a conexão única a ser realizada com o banco de dados indicado no arquivo de configuração por meio do método getConnection(). Se o arquivo de configuração não puder ser lido ou a conexão não puder ser realizada, será lançada uma exceção.

// SingleDBConn.java

// Singleton para conexão com BD import java.io.*;

import java.sql.*; import java.util.*;

public final class SingleDBConn { // referência única

private static SingleDBConn instance = null; // campo adicional

private static Connection connection = null; // construtor privado

private SingleDBConn() throws Exception { Properties prop = new Properties();

prop.load(new FileInputStream("config.properties")); String url = prop.getProperty("url");

connection = DriverManager.getConnection(url); }

// retorna conexão única

public static Connection getConnection() throws Exception { if (instance==null) { // "lazy-instantiation"

instance = new SingleDBConn(); }return connection;

}}

Exemplo 6.2 Classe SingleDBConn.

Aplicações que necessitem utilizar um banco de dados que exija uma única conexão podem fazer uso dessa classe que simplificará o controle de tal restrição.

(13)

417

Usos conhecidos e padrões relacionados

Usos conhecidos e padrões relacionados

Usos conhecidos e padrões relacionados

Usos conhecidos e padrões relacionados

Usos conhecidos e padrões relacionados

Toda máquina virtual possui uma única instância da classe java.lang.Runtime, que permite que a aplicação corrente acesse o ambiente no qual está sendo executada (Sun, 2001A). Como a JVM efetivamente opera em um único ambiente, só pode existir uma instância dessa classe, obtida pelo método getRuntime(). É claro que a classe Runtime implementa um padrão de projeto Singleton para cumprir com essa restrição.

O padrão Singleton, como implementado aqui, não garante uma única instância para diferentes máquinas virtuais, isto é, se em um sistema estivermos usando várias máquinas virtuais concorrentemente, o padrão Singleton garantirá uma instância da classe controlada para cada JVM ativa.

Esse padrão também pode ser utilizado para o controle de impressão (acesso controlado a uma certa fila de impressão), acesso restrito a portas de comunicação, uso de recursos restritos do sistema etc. Outros padrões de projeto podem ser construídos por meio do Singleton, tal como o AbstractFactory (6.6.9).

6.6.2 Iterator

Motivação, propósito e aplicabilidade

Motivação, propósito e aplicabilidade

Motivação, propósito e aplicabilidade

Motivação, propósito e aplicabilidade

Motivação, propósito e aplicabilidade

É um outro padrão de projeto relativamente simples, considerado como um padrão de

comportamento, muito útil para prover a navegação consistente entre elementos mantidos por uma coleção ou agregado de objetos, sem que sua estrutura se torne evidente ou que seja

necessário conhecimento de sua representação interna. Stroustrup afirma que: “iterators são a cola que mantêm containers e algoritmos juntos”. [Tradução livre] (Stroustrup, 1997, p. 549)

Existem inúmeras estruturas de dados que representam coleções, tais como arrays, listas ligadas, árvores etc. Da mesma forma, é freqüente a necessidade de percorrermos (ou navegarmos) os elementos armazenados por essas estruturas. Ao mesmo tempo, não é conveniente que o uso dessas estruturas nos obrigue a conhecer sua representação interna para possibilitar a navegação, nem que as interfaces dessas estruturas sejam oneradas com operações de alto nível para a realização dessa tarefa.

Para desacoplarmos as estruturas de dados das formas de sua navegação, propõe-se o padrão de projeto Iterator, cujos objetos serão responsáveis por prover o serviço de navegação pelos elementos da coleção a que se referem, sem expor a representação interna dessas estruturas nem violar seu encapsulamento.

(14)

418

Esse padrão pretende oferecer uma interface consistente para que os elementos de uma estrutura de dados possam ser adequadamente percorridos, ou seja, ele pode ser usado para prover o acesso ao conteúdo dessas estruturas sem violar seu encapsulamento e sem a necessidade de conhecimento da representação interna. Permite ainda que diferentes formas de navegação sejam implementadas e possibilita o controle de múltiplos percursos de navegação por uma mesma estrutura.

Uma boa prática de programação Java, que está se tornando comum, é a denominação de interfaces com o prefixo ‘IIIII’, seguido de um nome ou verbo representativo das habilidades especificadas pela interface (Coad et al., 1999, p. 83). Uma interface para coleções poderia ser chamada ICollectionICollectionICollectionICollectionICollection ou para o padrão Iterator poderia ser denominada IIteratorIIteratorIIteratorIIteratorIIterator. Em particular, esses nomes evitam criar confusão com os adotados pelo framework de coleções (java.util.Collectionjava.util.Collectionjava.util.Collectionjava.util.Collectionjava.util.Collection e java.util.Iteratorjava.util.Iteratorjava.util.Iteratorjava.util.Iteratorjava.util.Iterator) visto no Capítulo 1.

Estrutura, participantes e conseqüências

Estrutura, participantes e conseqüências

Estrutura, participantes e conseqüências

Estrutura, participantes e conseqüências

Estrutura, participantes e conseqüências

Na Figura 6.3, temos um diagrama de classes que ilustra a estrutura desse padrão.

Figura 6.3 Estrutura do padrão Iterator.

Existem quatro participantes nessa estrutura:

±

Interface ICollection

Especifica a operação responsável por retornar um objeto IIterator (getIterator()), o qual possibilitará realizar a navegação entre os elementos de implementações concretas das coleções (ICollection).

Pode especificar métodos adicionais às implementações de coleções.

(15)

419

±

Interface IIterator

Especifica as operações genéricas que permitirão realizar a navegação entre os elementos de uma coleção de objetos, no caso hasNext() (que verifica a existência de um próximo elemento) e next() (que retorna o próximo elemento existente).

±

Classe CollectionImpl

Implementação da interface ICollection que mantém um conjunto ou coleção de objetos em uma representação interna particular, que não precisa ser conhecida por seus usuários.

Por meio do método getIterator(), cria e retorna um objeto IteratorImpl apropriado, capaz de realizar a navegação entre seus elementos.

±

Classe IteratorImpl

Implementação da interface IIterator que oferece as operações nela definidas. Conhece a representação interna dos objetos CollectionImpl, permitindo uma forma consistente de navegação que oculta tal representação.

Efetivamente a classe IteratorImpl provê o serviço de navegação do conteúdo da estrutura de CollectionImpl, enquanto ICollection e IIterator definem as interfaces que devem ser

implementadas pelas classes concretas, possibilitando também tratamento polimórfico. A aplicação desse padrão apresenta várias conseqüências benéficas:

±

Diferentes estruturas de dados podem utilizar-se das mesmas interfaces providas por ICollection e IIterator para oferecer um serviço de navegação, cuja semântica é consistente.

±

Ficam claramente separadas as responsabilidades de armazenamento e navegação de uma estrutura de dados particular, simplificando tanto as implementações concretas de ICollection (armazenamento) como de IIterator (navegação).

±

Cada objeto do tipo IIterator pode manter um percurso de navegação independente para uma mesma coleção.

±

É possível implementarmos diferentes estratégias de navegação para uma mesma coleção (navegação em sentido reverso, bidirecional, ordenada etc.).

Implementação e exemplo

Implementação e exemplo

Implementação e exemplo

Implementação e exemplo

Implementação e exemplo

A implementação de uma interface IIterator define as operações gerais que devem estar

disponíveis nos objetos destinados a permitir a navegação pelos elementos de coleções e pode ser feita como é exibida no Exemplo 6.3. Identificamos no código a declaração do método hasNext(), que deve indicar a existência de um próximo elemento na coleção, e next(), que deve retornar o próximo elemento disponível.

(16)

420

// IIterator.java

// interface para navegação de coleções quaisquer public interface IIterator {

// verifica a existência de um próximo elemento public boolean hasNext();

// retorna o próximo elemento public Object next();

}

Exemplo 6.3 Interface Iiterator.

Neste padrão de projeto, as coleções devem ser dotadas de uma interface que minimamente inclua a operação que possibilita o retorno de objetos IIterator. Operações adicionais, como adição e remoção de elementos, poderiam ser também definidas, como no Exemplo 6.4, que mostra a interface ICollection.

// ICollection.java

// interface para obtenção de Iterator para coleções public interface ICollection {

// obtenção de um Iterator public IIterator getIterator();

// determina existência de um elemento public boolean has(Object object); // adição de um elemento

public boolean add(Object object); // remoção de um elemento

public boolean remove(Object object); // remoção de todos os elementos public void removeAll();

}

Exemplo 6.4 Interface Icollection.

As coleções, isto é, as estruturas de dados que conterão diversos elementos que adotarão a interface ICollection, devem prover uma implementação adequada para cada um dos métodos especificados, em particular para a operação getIterator(), que retornará um objeto que implementa a interface IIterator e que é capaz de permitir a navegação pelos elementos contidos por tal coleção. A classe ConjuntoLimitado, exibida no Exemplo 6.5, define uma estrutura de dados que pode armazenar um conjunto de objetos não nulos, limitado a um número predeterminado de elementos. A implementação dessa classe se baseia no uso de um array, cujo número de

elementos é determinado na instanciação do objeto por meio de um dos dois construtores

existentes. A estrutura ConjuntoLimitado implementa as operações de adição, pesquisa e remoção de elementos exigidas pela interface, mas isso não evidencia como internamente ocorre o

armazenamento dos elementos. A operação getIterator() retorna um objeto IIterator específico, na verdade uma implementação concreta da interface IIterator denominada

ConjuntoLimitadoIterator. Como proposto pelo padrão Iterator, a classe ConjuntoLimitado tem como responsabilidade a organização dos elementos, enquanto a classe ConjuntoLimitadoIterator tem como atribuição permitir a navegação pelos elementos contidos nos objetos

ConjuntoLimitado.

(17)

421

// ConjuntoLimitado.java

public class ConjuntoLimitado implements ICollection { // arrays para armazenamento do conteúdo do conjunto Object content[];

// construtor default public ConjuntoLimitado() {

this(10); }

// construtor parametrizado

public ConjuntoLimitado(int size) { content = new Object[size]; }

// determina posição de elemento no conjunto private int indexOf(Object object) {

for(int pos=0; pos<content.length; pos++) { if (content[pos]!=null) {

if (content[pos].equals(object)) { return pos;

} } }return -1; }

// verifica a existência de elemento no conjunto public boolean has(Object object) {

return indexOf(object)!=-1; }

// adição de novo elemento no conjunto public boolean add(Object object) {

if (object!=null) {

for(int pos=0; pos<content.length; pos++) { if (content[pos]==null) {

content[pos] = object; return true;

} } }return false; }

// remove elemento do conjunto

public boolean remove(Object object) { int pos = indexOf(object); if (pos != -1) {

content[pos] = null; return true;

}return false; }

// remove todos os elementos do conjunto

(18)

422

public void removeAll() {

for(int i=0; i<content.length; i++) { content[i] = null;

} }

// retorna Iterator específico do conjunto public IIterator getIterator() {

return new ConjuntoLimitadoIterator(this); }}

Exemplo 6.5 Classe ConjuntoLimitado.

A classe ConjuntoLimitadoIterator, que foi definida separadamente, como mostra o Exemplo 6.6, implementa a interface IIterator e armazena a referência do objeto ConjuntoLimitado que lhe deu origem, permitindo assim acessar os elementos contidos nessa coleção para prover o serviço de navegação desejado. Notem que o campo inteiro last mantém o controle de posição da

navegação, de forma que cada objeto ConjuntoLimitadoIterator possa tratar os percursos independentes.

// Iterator específico de ConjuntoLimitado

public class ConjuntoLimitadoIterator implements IIterator { private ConjuntoLimitado conjunto; // referência para conjunto private int last; // controle de posição

// construtor parametrizado

public ConjuntoLimitadoIterator(ConjuntoLimitado conjunto) { this.conjunto = conjunto;

last = -1; }

// verifica a existência de um próximo elemento public boolean hasNext() {

for(int pos=last+1; pos<conjunto.content.length; pos++) { if (conjunto.content[pos]!=null) {

return true; } }

return false; }

// retorna o próximo elemento public Object next() {

for(int pos=last+1; pos<conjunto.content.length; pos++) { if (conjunto.content[pos]!=null) {

last = pos;

return conjunto.content[pos]; } }

return null; }}

Exemplo 6.6 Classe ConjuntoLimitadoIterator.

(19)

423

O pequeno programa a seguir (Exemplo 6.7) cria um objeto ConjuntoLimitado, adiciona e remove alguns elementos por meio dos métodos da interface ICollection e utiliza um objeto IIterator para percorrer e exibir seu conteúdo.

// TestaConjunto.java

public class TestaConjunto {

public static void main(String a[]) { // instancia coleção

ICollection conjunto = new ConjuntoLimitado();

System.out.println("Adicionando objetos ao Conjunto..."); for(int i=0; i<6; i++) {

conjunto.add("Objeto#"+i); }mostraConjunto(conjunto);

System.out.println("Removendo objetos do Conjunto..."); for(int i=0; i<6; i+=2) {

conjunto.remove("Objeto#"+i); }mostraConjunto(conjunto);

System.out.println("Adicao extra..."); conjunto.add("Objeto#100");

mostraConjunto(conjunto); }

public static void mostraConjunto(ICollection conjunto) { System.out.println("Conteudo:");

IIterator ii = conjunto.getIterator(); while(ii.hasNext()) {

System.out.print(ii.next()+" "); }System.out.println("\n");

}}

Exemplo 6.7 Classe TestaConjunto.

Executando este programa, teríamos:

>java TestaConjunto

Adicionando objetos ao Conjunto... Conteudo:

Objeto#0 Objeto#1 Objeto#2 Objeto#3 Objeto#4 Objeto#5 Removendo objetos do Conjunto...

Conteudo:

Objeto#1 Objeto#3 Objeto#5 Adicao extra...

Conteudo:

Objeto#100 Objeto#1 Objeto#3 Objeto#5

(20)

424

Devemos ressaltar que, para possibilitar que a classe ConjuntoLimitadoIterator tivesse acesso ao conteúdo dos objetos ConjuntoLimitado, o array interno destinado ao armazenamento do conteúdo foi implicitamente declarado com acesso package. Se tal campo fosse declarado como private, não seria possível o acesso necessário por parte da classe ConjuntoLimitadoIterator, e se fosse

declarado como public, violaríamos seu encapsulamento, pois quaisquer classes teriam acesso direto ao array destinado a seu conteúdo. Mas o problema não foi resolvido, pois classes existentes no mesmo pacote também têm acesso a esse array.

A solução desse problema vem com o uso das classes internas (inner classes). Se a classe que implementará a interface IIterator for interna, a mesma tem acesso às estruturas internas da classe ConjuntoLimitado (a classe externa ou outter class) mesmo que o array destinado a conteúdo seja declarado com acesso privado, garantindo seu encapsulamento. Isso acontece porque toda instância de uma classe interna (inner class) possui uma referência implícita para a instância da classe externa (outter class) que a originou. Para preservarmos a implementação anterior das classes ConjuntoLimitado e ConjuntoLimitadoIterator, criaremos uma classe ConjuntoLimitado2, com a mesma funcionalidade de ConjuntoLimitado, com uma classe interna IIteratorImpl, que implementa a interface IIterator. A classe interna é declarada privada para que suas instâncias só possam ser obtidas por meio da operação getIterator() da classe externa. Como o construtor parametrizado que existia anteriormente não mais receberá uma referência da coleção, o mesmo passa a ser default (sem parâmetros). Modificando-se a inicialização do campo last, podemos eliminar tal construtor, simplificando a implementação do IIterator.

// ConjuntoLimitado2.java

public class ConjuntoLimitado2 implements ICollection { // arrays para armazenamento do conteúdo do conjunto private Object content[];

// construtor default

public ConjuntoLimitado2() { this(10);

}

// construtor parametrizado

public ConjuntoLimitado2(int size) { content = new Object[size]; }

// determina posição de elemento no conjunto private int indexOf(Object object) {

for(int pos=0; pos<content.length; pos++) { if (content[pos]!=null) {

if (content[pos].equals(object)) { return pos;

} } }return -1; }

// verifica a existência de elemento no conjunto public boolean has(Object object) {

(21)

425

return indexOf(object)!=-1; }

// adição de novo elemento no conjunto public boolean add(Object object) {

if (object!=null) {

for(int pos=0; pos<content.length; pos++) { if (content[pos]==null) {

content[pos] = object; return true;

} } }return false; }

// remove elemento do conjunto

public boolean remove(Object object) { int pos = indexOf(object); if (pos != -1) {

content[pos] = null; return true;

}return false; }

// remove todos os elementos do conjunto public void removeAll() {

for(int i=0; i<content.length; i++) { content[i] = null;

} }

// retorna Iterator específico do conjunto public IIterator getIterator() {

return new IIteratorImpl(); }

// Iterator específico de ConjuntoLimitado

private class IIteratorImpl implements IIterator { private int last = -1; // controle de posição // verifica a existência de um próximo elemento public boolean hasNext() {

for(int pos=last+1; pos<content.length; pos++) { if (content[pos]!=null) {

return true; } }

return false; }

// retorna o próximo elemento public Object next() {

for(int pos=last+1; pos<content.length; pos++) { if (content[pos]!=null) {

last = pos;

(22)

426

return content[pos]; } }

return null; } }

}

Exemplo 6.8 Classe ConjuntoLimitado2.

O uso da classe Conjunto2 é idêntico ao da classe Conjunto, com a vantagem adicional do encapsulamento melhorado de sua estrutura interna.

No diretório de exemplos do Capítulo 6, temos o programa ConjuntoDemoConjuntoDemoConjuntoDemoConjuntoDemoConjuntoDemo, uma aplicação Swing, que permite demonstrar interativamente as capacidades da coleção ConjuntoLimitado2ConjuntoLimitado2ConjuntoLimitado2ConjuntoLimitado2.ConjuntoLimitado2

Usos conhecidos e padrões relacionados

Usos conhecidos e padrões relacionados

Usos conhecidos e padrões relacionados

Usos conhecidos e padrões relacionados

Usos conhecidos e padrões relacionados

O framework de coleções (mostrado no Capítulo 1) faz uso intensivo desse padrão: a interface java.util.Collection, raiz desse framework, especifica a operação iterator() que retorna um objeto que implementa a interface java.util.Iterator (Sun, 2001A). Sendo assim, as principais interfaces derivadas de Collection, que são Set e List, também exigem essa operação, assim como as classes abstratas AbstractCollection, AbstractSet e AbstractList. Cada implementação concreta fornece por meio da operação iterator() um objeto que implementa a interface Iterator e que é especializado na navegação pelos elementos dessa coleção. Tais objetos Iterator retornados são usualmente capazes de realizar algumas operações adicionais, tais como remoção ou modificação de elementos da coleção.

Nas versões do Java (anteriores à introdução do framework de coleções), a interface java.util.Enumerationjava.util.Enumerationjava.util.Enumerationjava.util.Enumerationjava.util.Enumeration era utilizada pelos objetos atualmente implementados como IteratorIteratorIteratorIteratorIterator. As classes java.util.StringTjava.util.StringTjava.util.StringTjava.util.StringTokenizerjava.util.StringTokenizerokenizerokenizerokenizer e java.util.V

java.util.Vjava.util.V java.util.V

java.util.Vectorri:ectorri:ectorri:ectorri:ectorri: ainda utilizam objetos EnumerationEnumerationEnumerationEnumerationEnumeration.

Ainda dentro da API Java, temos a interface java.sql.ResultSet, pertencente ao JDBC — Java DataBase Connectivity —, que oferece um sofisticado suporte à navegação de resultados

retornados pela execução de consultas em bancos de dados. A STL — Standard Template Library — da linguagem C++ também utiliza o conceito de containers, semelhante ao de coleção, cujos objetos contidos podem ser percorridos por meio do objetos Iterator, separando a implementação de tais estruturas das responsabilidades de navegação por seus elementos (Stroustrup, 1997, p. 549).

Outros padrões que podem ser relacionados ao Iterator são: Decorator (6.6.6) e Factory Method (6.6.3) ou Memento. (Gamma et al., 1995, p. 283)

(23)

427

6.6.3 Factory Method

Motivação, propósito e aplicabilidade

Motivação, propósito e aplicabilidade

Motivação, propósito e aplicabilidade

Motivação, propósito e aplicabilidade

Motivação, propósito e aplicabilidade

Uma estratégia comum no desenvolvimento de sistemas é utilizar uma determinada classe como base para a criação de várias outras subclasses mais especializadas. A base dessa hierarquia torna- se uma interface comum para os tipos derivados, intenção que pode ser reforçada tornando-a uma classe abstrata ou uma interface. A adição de novos tipos pode seguir o mesmo processo, sem que seja afetado o código existente. Mas existe um problema na criação de objetos dessa hierarquia, pois nos pontos onde um objeto desse tipo é criado, como antecipar a adição de novos tipos, isto é, como selecionar uma dentre as classes disponíveis se, possivelmente, novas implementações são incorporadas ao sistema?

O padrão Factory Method (que pode ser conhecido como método fábrica) trata exatamente dessa questão, definindo uma interface para criação de objetos, agora entendidos como ‘produtos’, deixando que uma classe especial (a ‘fábrica’) decida qual classe será utilizada na instanciação de objetos, isolando o ponto onde se requisitam novos objetos daquele onde ocorre a seleção dos tipos existentes para sua criação. Assim, na implementação dessa classe especial ‘fábrica’, deverá existir um método responsável pela fabricação dos ‘produtos’, o método fábrica, que cria objetos pertencentes a uma família de classes, sendo assim um padrão de criação.

Essa solução é aplicável quando existem classes que não podem prever a disponibilidade de novos tipos para a criação de objetos; quando o uso desses objetos se dá por meio de uma interface comum, importando assim como ocorre a criação dos mesmos; e é desejável que disponhamos de classes especiais que possam decidir pelo tipo de objeto a ser criado.

Estrutura, participantes e conseqüências

Estrutura, participantes e conseqüências

Estrutura, participantes e conseqüências

Estrutura, participantes e conseqüências

Estrutura, participantes e conseqüências

Podemos notar a existência de quatro componentes distintos na estrutura desse padrão (ilustrada na Figura 6.4), que são descritos a seguir.

±

Interface IProduct

Especifica a interface comum para os tipos especializados que deverão ser instanciados.

±

Classe(s) ProductImpl

Uma ou mais classes concretas que implementa a interface IProduct, dotando seus objetos de comportamento e características especializadas.

±

Interface IFactory

Especifica a interface para obtenção de objetos que implementam a interface IProduct, por exemplo uma operação createProduct() (o método fábrica) que retorna objetos de tal tipo.

(24)

428

±

Classe FactoryImpl

Classe concreta que implementa a interface de obtenção de objetos que por sua vez implementam a interface IProduct, ou seja, a operação createProduct();

A operação criaProduto() é responsável por selecionar qual classe concreta será utilizada para a criação de um objeto IProduct.

Figura 6.4 Estrutura do padrão Factory Method.

Uma conseqüência direta do uso desse padrão é a dissociação entre a aplicação e as classes específicas, concentrando no método fábrica tal conhecimento. Neste sentido, a classe FactoryImpl pode ser utilizada pela aplicação propriamente dita, que por meio dos métodos ‘fábrica’ obtém objetos desejados. 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.

Implementação e exemplo

Implementação e exemplo

Implementação e exemplo

Implementação e exemplo

Implementação e exemplo

O padrão Factory Method, como vimos, exige que tenhamos duas interfaces para definição do acesso aos participantes IProduct e IFactory, relativas aos seus produtos e às fábricas. Imaginando que os produtos desejados são coleções, poderíamos tomar a interface ICollection (Exemplo 6.4) como equivalente à interface IProduct da estrutura desse padrão.

Todos os ‘produtos’ que serão fornecidos pelo Factory Method devem assim implementar a interface ICollection, tal como a classe ConjuntoLimitado2, dada no Exemplo 6.8, que possui todas as operações indicadas por ICollection. Analogamente, poderíamos implementar outra classe de

(25)

429

‘produto’, tal como ConjuntoNaoLimitado, destinada a oferecer um conjunto com número máximo de elementos variável, como ilustrado no Exemplo 6.9. Essa nova implementação é baseada em uma lista ligada simples em vez de um array.

// ConjuntoNaoLimitado.java

public class ConjuntoNaoLimitado implements ICollection { // arrays para armazenamento do conteúdo do conjunto private Node start = null;

// verifica a existência de elemento no conjunto public boolean has(Object object) {

Node element = start; while (element!=null) {

if (element.content.equals(object)) { return true;

}element = element.next; }return false;

}

// adição de novo elemento no conjunto public boolean add(Object object) {

if (object!=null) {

start = new Node(object, start); return true;

}return false; }

// remove elemento do conjunto

public boolean remove(Object object) { if (start!=null) {

if (start.content.equals(object)) { Node toDelete = start;

start = start.next; toDelete = null; return true; } else {

Node last = start; while (last.next!=null) {

if (last.next.content.equals(object)) { Node toDelete = last.next;

last.next = last.next.next; toDelete = null;

return true; }last = last.next; } }

}return false; }

// remove todos os elementos do conjunto

(26)

430

public void removeAll() { while (start!=null) {

Node toDelete = start; start = start.next; toDelete = null; } }

// retorna Iterator específico do conjunto public IIterator getIterator() {

return new IIteratorImpl(); }

// Nó para lista ligada de elementos do ConjuntoNaoLimitado private class Node {

public Object content; public Node next;

public Node(Object content, Node next) { this.content = content;

this.next = next; } }

// Iterator específico de ConjuntoNaoLimitado private class IIteratorImpl implements IIterator {

private Node next = start; // controle de posição // verifica a existência de um próximo elemento public boolean hasNext() {

return next!=null; }

// retorna o próximo elemento public Object next() {

if (hasNext()) {

Object tmp = next.content; next = next.next;

return tmp; }return null; } }

}

Exemplo 6.9 Classe ConjuntoNaoLimitado.

Para evitarmos que as aplicações tenham que especificar diretamente as classes concretas que implementam a interface ICollection, no caso ConjuntoLimitado2 e ConjuntoNaoLimitado, a criação de objetos ‘produto’ se fará por meio de classes que deverão implementar a outra interface necessária a esse padrão, denominada por exemplo ICollectionFactory, a qual especifica o método que fabricará tais objetos. Essa interface está relacionada no Exemplo 6.10, onde podemos notar que createCollection(String) é o método ‘fábrica’, o qual recebe um objeto String indicativo do produto desejado (a coleção). Se o objeto fornecido não indicar um produto disponível (uma implementação conhecida de ICollection), será lançada a exceção de execução

(27)

431

CollectionNotAvailableException, criada com o propósito exclusivo de sinalizar essa situação (veja o Exemplo 6.11).

// ICollectionFactory.java

public interface ICollectionFactory { // retorna "produtos" disponíveis

public String[] getAvailableCollections(); // cria um "produto" conforme tipo dado

public ICollection createCollection(String collectionName) throws CollectionNotAvailableException;

}

Exemplo 6.10 Interface IcollectionFactory.

// CollectionNotAvailableException.java

public class CollectionNotAvailableException extends RuntimeException { public CollectionNotAvailableException(String message) {

super(message); }}

Exemplo 6.11 Classe CollectionNotAvailableException.

Ainda na interface ICollectionFactory adicionamos um outro método, denominado

getAvailableCollections(), que permite retornar um array de objetos String válidos para seleção das coleções. Dessa maneira, é possível que as aplicações que necessitem utilizar coleções distintas possam determinar quais tipos estão disponíveis ou serem configuradas diferentemente sem efetivo conhecimento das classes envolvidas.

A exceção CollectionNotAvailableException possui apenas um construtor parametrizado com a mensagem de erro, que aciona o construtor equivalente de sua superclasse

java.lang.RuntimeException. Notemos que, sendo uma exceção de execução, seu tratamento não é obrigatório o que simplifica seu uso.

Uma implementação possível da interface ICollectionFactory é dada no Exemplo 6.12. Um array de objetos String, com a identificação dos tipos de coleções disponíveis (tais strings são livres, ou seja, não precisam ter relação com o nome das classes concretas associadas). O método

getAvailableCollections() retorna uma referência para esse array, o qual é declarado final para que seus objetos não possam ser alterados por meio da referência retornada. O método

createCollection(String), mediante o recebimento de uma identificação válida, instancia o objeto usando a classe concreta adequada; caso contrário, lança a exceção

CollectionNotAvailableException. // CollectionFactoryImpl.java

public class CollectionFactoryImpl implements ICollectionFactory { // tipos disponíveis

private final String types[] =

{ "ConjuntoLimitado", "ConjuntoNaoLimitado" }; // retorna "produtos" disponíveis

public String[] getAvailableCollections() {

(28)

432

return types; }

// cria um "produto" conforme tipo dado

public ICollection createCollection(String collectionName) throws CollectionNotAvailableException {

if (collectionName.equals(types[0])) return new ConjuntoLimitado2(); if (collectionName.equals(types[1]))

return new ConjuntoNaoLimitado();

throw new CollectionNotAvailableException(collectionName); }}

Exemplo 6.12 Classe CollectionFactoryImpl.

A aplicação TestaConjunto, dada no Exemplo 6.7, opera uma coleção exclusivamente por meio da interface ICollection, embora na instanciação da coleção utilize diretamente o nome da classe de uma das implementações concretas dessa interface, no caso ConjuntoLimitado, conforme o trecho destacado:

// instancia coleção

ICollection conjunto = new ConjuntoLimitado();

Se quiséssemos que essa aplicação utilizasse outra implementação, por exemplo

ConjuntoLimitado2, a mesma deveria ser modificada. No caso temos uma modificação muito simples, localizada em uma única linha, mas se ocorresse em um sistema de maior porte, todas as operações de instanciação de tal coleção deveriam ser localizadas e modificadas, o que obviamente é um trabalho mais complexo e também sujeito a erros (alguns pontos poderiam ser esquecidos). Utilizando uma implementação de uma fábrica de coleções (Exemplo 6.12), tal código seria substituído por:

// instancia fábrica de coleções

ICollectionFactory fabrica = new CollectionFactoryImpl(); // obtém coleção

ICollection conjunto = fabrica.createCollection("ConjuntoLimitado");

Com esse trecho de código, a aplicação TestaConjunto não teria que ser modificada quando substituíssemos a classe ConjuntoLimitado por ConjuntoLimitado2. Apenas a implementação da fábrica (CollectionFactoryImpl) deveria ser modificada em um único ponto. Se para TestaConjunto parece que apenas deslocamos o local da alteração dessa classe para a implementação da fábrica, se novamente considerarmos um sistema complexo, todas as modificações decorrentes do uso direto de classes concretas seriam reduzidas a uma modificação simples na implementação da

(29)

433

fábrica, evidenciando a enorme utilidade do padrão Factory Method no desacoplamento da necessidade de criação de objetos em relação às classes efetivamente usadas em sua instanciação.

Por questões de simplicidade e conveniência, é comum que se dispense a implementação da interface IFactory

IFactoryIFactory IFactory

IFactory na construção da classe fábrica (no caso CollectionFactoryImplCollectionFactoryImplCollectionFactoryImplCollectionFactoryImpl). Para tanto, são definidos apenas osCollectionFactoryImpl métodos estáticos para criação de produtos, eliminando a necessidade de instanciarmos um objeto ‘fábrica’ para que seus métodos possam ser usados.

É importante notarmos que, embora simplifique o uso do padrão Factory Method, essa forma passa a não mais contar com uma interface comum a outras fábricas.

Uma implementação alternativa da fábrica com apenas métodos estáticos para obtenção de produtos poderia ser como segue:

// CollectionFactoryImpl2.java

public class CollectionFactoryImpl2 { // cria ConjuntoLimitado

public static ICollection createConjuntoLimitado(int tam) { return new ConjuntoLimitado2(tam);

}// cria ConjuntoNaoLimitado

public static ICollection createConjuntoNaoLimitado() { return new ConjuntoNaoLimitado();

}}

Exemplo 6.13 Classe CollectionFactoryImpl2.

O uso dessa nova fábrica seria como nos trechos dados abaixo: // obtenção de conjunto limitado

ICollection conj1 = CollectionFactoryImpl2.createConjuntoLimitado(2); // obtenção de conjunto não limitado

ICollection conj2 = CollectionFactoryImpl2.createConjuntoNaoLimitado();

Usos conhecidos e padrões relacionados

Usos conhecidos e padrões relacionados

Usos conhecidos e padrões relacionados

Usos conhecidos e padrões relacionados

Usos conhecidos e padrões relacionados

Na API do Java existem vários exemplos de aplicação desse padrão. No Swing, os componentes podem receber bordas (aplicação do padrão Decorator), as quais podem ser obtidas por meio do uso dos vários métodos fábrica estáticos existentes na classe javax.swing.BorderFactory, sem que o usuário tenha que conhecer quais classes concretas foram especificamente utilizadas,

manipulando-as conforme a interface javax.swing.border.Border. As classes Socket e

ServerSocket, do pacote java.net, têm suas instâncias obtidas através de fábricas, cuja interface é dada por SocketImplFactory (Sun, 2001A).

(30)

434

Um padrão intimamente ligado ao Factory Method é o Abstract Factory (6.6.9), ambos muito utilizados em frameworks e toolkits (Gamma et al., 1995, p. 115).

6.6.4 Composite

Motivação, propósito e aplicabilidade

Motivação, propósito e aplicabilidade

Motivação, propósito e aplicabilidade

Motivação, propósito e aplicabilidade

Motivação, propósito e aplicabilidade

Em inúmeras situações, é necessário lidarmos com conjuntos de elementos agregados sob a forma de composições, por exemplo: peças que agrupadas formam conjuntos simples, que agregados a outros conjuntos e peças podem formar conjuntos mais sofisticados, e assim por diante, em uma estrutura que não é uma simples coleção de elementos, mas uma hierarquia particular de coleções de elementos, tal como em uma máquina qualquer (automóvel ou relógio de parede). Outra característica importante é que as composições podem ser tratadas como um único elemento, facilitando sua manipulação.

Essas situações também ocorrem com freqüência no desenvolvimento de software: como sistemas gráficos, onde elementos simples, (retângulos, elipses, linhas e texto) podem ser agrupados formando figuras mais complexas, e estas podem compor desenhos; ou interfaces gráficas, onde componentes especiais (compartimentos ou contêiners) podem conter outros componentes, até mesmo outros contêiners, de forma a termos painéis, janelas ou diálogos.

As composições são o que chamamos de padrão de projeto Composite. Segundo Gamma et al. (1995, p. 163), esse padrão de estrutura ‘compõe objetos estruturados como árvore para

representar hierarquias elemento/conjunto’. Podem, portanto, ser empregadas nas situações onde necessitamos representar hierarquias de conjuntos de objetos, de forma que possam ser ignoradas as diferenças entre a composição em si e os componentes individuais, facilitando e uniformizando seu tratamento.

Estrutura, participantes e conseqüências

Estrutura, participantes e conseqüências

Estrutura, participantes e conseqüências

Estrutura, participantes e conseqüências

Estrutura, participantes e conseqüências

Este padrão conta com quatro participantes:

±

Classe Abstrata Component

Especifica o comportamento esperado por todos os componentes presentes na hierarquia. Especifica a operação getParent() para determinação do componente pai (aquele que contém o componente filho).

Pode especificar ou implementar outras operações comuns à hierarquia.

±

Classe ContainerImpl

Especifica e implementa as operações necessárias para os componentes que conterão outros componentes (os contêiners);.

Armazena os componentes contidos.

Implementa as operações especificadas para os componentes comuns em termos de sua execução para todos os componentes contidos.

Pode substituir a implementação das operações comuns.

(31)

435

±

Classe ComponentImpl

Implementa as operações necessárias para os componentes comuns (aqueles que não podem conter outros componentes).

±

Classe Client

Constrói composições de componentes por meio das operações gerais, definidas para todos os componentes, ou das específicas dos containers, destinadas ao agrupamento de componentes.

Os quatro participantes do padrão Composite estão representados na estrutura ilustrada na Figura 6.5, que é uma variação da versão clássica proposta por Gamma et al. (1995, p. 164).

Figura 6.5 Estrutura do padrão Composite.

A estrutura clássica indica que as operações de adição, remoção e controle de componentes na composição são especificadas na classe abstrata Component, e assim disponíveis tanto nas folhas (classe

ComponentImpl ComponentImplComponentImpl ComponentImpl

ComponentImpl) como nas composições (classe ContainerImplContainerImplContainerImplContainerImpl). Isso exige que providências especiais sejamContainerImpl tomadas quando tentamos inadvertidamente usar tais operações em componentes que não têm essa capacidade, provavelmente lançando mensagens de erro.

Desta forma o Client pode construir hierarquias de componentes, manipulando-as genericamente como unidades, sem se preocupar com o fato de estar lidando com um ContainerImpl ou com um ComponentImpl, o que simplifica o código. Além disso, é possível que novos componentes sejam adicionados à hierarquia, podendo ser tratados da mesma forma e sem exigir modificações no Client. Apenas para execução das operações de controle de componentes é que será necessária a

Referências

Documentos relacionados

As informações básicas sobre a forma de acesso às referências bibliográficas e ao material de apoio, as propostas de atividades, bem como os aplicativos necessários para

•   O  material  a  seguir  consiste  de  adaptações  e  extensões  dos  originais  gentilmente  cedidos  pelo 

iv. Desenvolvimento de soluções de big data aplicadas à gestão preditiva dos fluxos de movimentação portuária de mercadorias e passageiros. d) Robótica oceânica: criação

Subordinada a Diretoria Executiva, tem como competência as atividades de desenvolvimento de protocolo, registros dos profissionais médicos veterinários e zootecnistas, registro

Este trabalho buscou, através de pesquisa de campo, estudar o efeito de diferentes alternativas de adubações de cobertura, quanto ao tipo de adubo e época de

O enfermeiro, como integrante da equipe multidisciplinar em saúde, possui respaldo ético legal e técnico cientifico para atuar junto ao paciente portador de feridas, da avaliação

Apothéloz (2003) também aponta concepção semelhante ao afirmar que a anáfora associativa é constituída, em geral, por sintagmas nominais definidos dotados de certa

A abertura de inscrições para o Processo Seletivo de provas e títulos para contratação e/ou formação de cadastro de reserva para PROFESSORES DE ENSINO SUPERIOR