Escolar Documentos
Profissional Documentos
Cultura Documentos
Padrões de Projeto:
Introdução aos Padrões da “Gangue dos Quatro”
Fábio Nogueira de Lucena
fabio@inf.ufg.br
19/11/2007
Instituto de Informática (UFG)
Resumo
O livro Design Patterns: Elements of Reusable ObjectOriented Software foi escrito por Erich
Gamma, Richard Helm, Ralph Johnson e John Vlissides. Estes autores são conhecidos por “gangue
dos quatro”, ou pela abreviação da expressão em inglês GoF (abreviação amplamente empregada
neste texto). Este livro é um clássico quando o assunto é padrão de projeto. Editado pela Addison
Wesley em 1995, contém 23 padrões de projeto. Apesar de celebridade, a leitura é difícil,
principalmente para iniciantes.
O presente texto não apresenta nenhum conteúdo novo acerca destes 23 padrões. Também não espere
que tudo apresentado no livro da GoF seja aqui reapresentado. Este texto não dispensa a leitura do
livro da GoF. Leitores que estão iniciando seus estudos com padrões de projeto, contudo, deverão
encontrar mais facilidade ao consultar este documento com o acompanhamento do livro da GoF. Esta
é a pretensão deste texto: legibilidade.
Há várias referências na literatura com propósitos similares que também contribuíram na confecção
deste documento. The Design Patterns Java Companion, James W. Cooper, AdissonWesley, 1998 e
Thinking in Patterns: ProblemSolving Techniques Using Java, Bruce Eckel, MindSet, Inc. Convém citar
uma terceira fonte, cuja leitura estimula a imaginação: Design Patterns Explained: A New Perspective on
ObjectOriented Design, Allan Shalloway & James R. Trott, AddisonWesley, 2002. Por último, Software
Architecture Design Patterns in Java, Partha Kuchana, Auerbach Publication, 2004, reapresenta os 23
padrões da GoF, dentre outros. É deste último livro a definição de padrão de projeto, fornecida abaixo:
“Um padrão de projeto é uma boa prática documentada ou núcleo de uma solução que foi aplicada
satisfatoriamente em vários contextos para resolver um problema recorrente em um conjunto
específico de situações.” (Partha Kuchana)
1 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Introdução
Os padrões da GoF são classificados em padrões de criação, padrões estruturais e padrões de
comportamento. A distribuição dos padrões por esta classificação é discutida abaixo e precede a
discussão dos padrões de projeto proriamente ditos.
Convém reafirmar que o propósito deste texto é apresentar um acompanhamento para a leitura do
livro da GoF. Não se tem o propósito de substituir aquele livro por este texto, que pode ser visto como
um complemento, em vez de substituto.
Também é importante esclarecer que os 23 padrões discutidos no livro da GoF não são os únicos
disponíveis nem os “melhores” padrões que existem. Em http://hillside.net/ o leitor terá acesso a
muitos outros padrões, assim como informações úteis ao emprego de padrões. Em tempo, padrão de
projeto orientado a objeto é uma proposta de solução para um problema recorrente e, em particular,
faz uso de orientação a objetos.
A atividade de projetista de software orientado a objetos, de tempos em tempos, se depara com
problemas ou dificuldades freqüentes. Soluções foram propostas e validadas para alguns destes
problemas. Com o propósito de socializar estas soluções e permitir a reutilização do conhecimento já
experimentado, o par problema e solução foi empacotado no que denominamos de padrão.
Para empregar um padrão, portanto, é necessário a existência de um problema. Ou seja, o projetista
deverá ter a habilidade para, dado determinado contexto, identificar a viabilidade ou não do emprego
de um padrão. Este é um procedimento análogo ao de um médico ao prescrever um medicamento.
Padrão não é um remédio para todos os males, mas apenas para um em particular.
O emprego da abordagem proposta por um padrão também exige trabalho intelectual. Embora a
estrutura da solução seja parte do padrão, detalhes peculiares ao contexto onde este será empregado
estarão presentes na solução e não podem ser antecipados. Alterta aos leitores: as soluções
apresentadas não estão prontas para o consumo como apresentadas, in natura. Será necessário
“moldar” a solução para o caso investigado.
O elemento mais relevante de um padrão é a compreensão dele e não uma ou outra implementação em
particular. Recorrendo ao paralelo com a medicina, médicos prescrevem medicamentos com base em
um princípio ativo contido no remédio. É preciso conhecer o remédio. Se esta compreensão for
atingida, então o leitor deverá estar apto a empregar o padrão na sua linguagem de programação de
preferência, embora aquelas orientadas a objetos como Visual Basic .NET® e Delphi®, por exemplo,
ofereçam construções desejáveis para esta finalidade. Neste texto, os exemplos são fornecidos em
Java (http://java.sun.com) e os modelos fazem uso da UML (http://www.uml.org).
Por último, os nomes dos padrões não foram traduzidos para o português, não há uma justificativa
razoável. Contudo, saberemos qual o padrão que está sendo discutido.
Padrões de criação
Software orientado a objeto faz uso de objetos. Objetos só existem porque foram criados a partir de
classes. Em Java, por exemplo, a criação de uma instância da classe Livro pode ser realizada pela
sentença abaixo.
Livro l = new Livro();
Nem sempre esta forma é suficiente. Os padrões de criação, todos eles, mostram como criar instâncias
em cenários mais elaborados. Estes cenários são abstraídos em padrões de criação.
2 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
São cinco os padrões de criação: Abstract Factory (8), Builder (11), Factory (3), Prototype (13) e
Singleton (14).
Padrões estruturais
Objetos são obtidos de classes, que podem ser numerosas e apresentar relacionamento complexo entre
elas. Padrões estruturais apresentam organizações elegantes onde classes são combinadas para formar
estruturas maiores. A idéia é oferecer combinações gerenciáveis de classes.
São sete os padrões estruturais: Adapter (17), Bridge (11), Composite (26), Decorator (28), Façade
(34), Flyweight (45) e Proxy (35).
Padrões de comportamento
Só existem objetos porque foram criados, talvez com o apoio de padrões de criação. O relacionamento
entre objetos pode ser sofisticado e capturado em padões estruturais. A interação entre os objetos, ou
seja, a troca de mensagens entre eles, também pode ser complexa, exigindo que cenários comuns
possam ser capturados em padrões de comportamento.
São onze os padrões de comportamento: Chain of responsibility (38), Command (40), Interpreter (37),
Iterator (42), Mediator (44), Memento (45), Observer (47), State (50), Strategy (53), Template (37) e
Visitor (56).
Factory (criação)
Também conhecido por Factory Method, este padrão encapsula a responsabilidade pela criação de um
objeto quando há opção de qual classe empregar. O Factory ainda é empregado quando várias
atividades estão envolvidas na criação da instância desejada como a definição de valores iniciais, por
exemplo. Nestes casos, em vez de exigir que a decisão seja do conhecimento de todo e qualquer
cliente, assim como detalhes da montagem de um objeto válido, tais operações são encapsuladas em
um Factory (Method).
Este padrão retorna uma instância de uma classe, dentre um conjunto de classes possíveis. A escolha
da classe é baseada em informações fornecidas ao padrão. Em geral, todas as classes das quais uma
instância pode ser gerada possuem uma classe ancestral comum ou implementam uma interface
comum.
3 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Especialmente quando a instância desejada implementa uma interface I ou é subclasse de uma classe
C, a identificação da implementação de I ou da subclasse de C a ser empregada, pode envolver lógica
que não é convenientemente fornecida em um objeto do domínio da aplicação. Por exemplo, a classe
Impressora, pelo princípio de projeto conhecido por separation of concerns, não deveria ser
responsável, ela própria, pela identificação da classe específica que trata dos serviços oferecidos por
determinado tipo de impressora. Esta identificação pode ficar encapsulada em um Factory.
Isto porque para a semântica da aplicação esta decisão pode envolver considerações técnicas e não do
domínio, como no exemplo fornecido. Também pode haver interesse em otimizar a gerência de
memória e, em conseqüência, reutilizar objetos. O código que implementa esta gerência não seria
adequadamente fornecido em uma classe do domínio da aplicação.
Nestes casos, fazer com que uma classe do domínio realize a decisão é acrescentar a tal classe uma
responsabilidade alheia ao seu objetivo, o que reduz a coesão desta classe. Em conseqüência, melhor é
criar uma classe que crie a instância desejada, o Factory, que não faz sentido para o domínio da
aplicação, mas oferece serviço necessário para a aplicação. Este padrão permite manter interesses
distintos em classes distintas, o que favorece a coesão das classes resultantes.
A implementação do padrão Factory exige a criação de uma classe, geralmente denominada de
Xfactory, onde X é o identificador da classe a partir da qual uma instância deve ser criada. Convém
ressaltar que X também pode ser uma superclasse da classe a partir da qual a instância desejada será
criada. Também é possível que X seja uma interface e, neste caso, o resultado será um objeto de uma
classe que implementa X.
Seguindo esta convenção, a classe AnimalFactory cria instâncias de Animal, seja da classe Animal
propriamente dita ou de alguma subclasse como Gato e Cachorro. Uma fábria de instâncias de
Pessoa pode envolver lógica considerável para validar a operação. Tal lógica deverá estar
encapsulada na classe PessoaFactory. Criar instâncias de classes que implementam a interface
TrataArquivo é uma atribuição típica de uma classe denominada de TrataArquivoFactory e assim
por diante. A figura abaixo apresenta os modelos destes exemplos.
Animal <<Interface>>
Pessoa TrataArquivo
Implementação
A implementação do padrão Factory deve oferecer flexibilidade quanto à classe de origem da
instância a ser criada. Ou seja, a lógica de criação da instância provavelmente irá escolher entre as
implementações disponíveis de uma interface ou entre as subclasses da classe desejada. Por exemplo,
dependendo do sistema operacional empregado será criada uma instância de LinuxFile, WinFile ou
MacOsFile. A escolha poderá ser determinada por uma estrutura de decisão, por exemplo, if. Neste
caso, a condição seria responsável por determinar qual o sistema operacional empregado.
Alternativamente, podese obter a informação, a classe a ser empregada pelo Factory, de um arquivo.
O nome da classe pode ser depositada, por exemplo, na variável nomeClasse do tipo String. Em
Java, criar uma instância de uma classe através do nome desta é uma tarefa que pode ser realizada
com a sentença abaixo.
4 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Para a sentença acima, assuma que nomeClasse contém o identificador da classe LinuxFile,
WinFile ou MacOsFile. Deste identificador é possível obter um objeto que representa a classe
correspondente. A partir deste objeto podese criar instâncias via newInstance(). Observe que o
retorno é moldado para a interface TrataArquivo e a referência depositada em obj, que deverá ser o
retorno do método getTrataArquivo() da classe TrataArquivoFactory. A figura abaixo ilustra
este cenário.
TrataArquivoFactory
<<Interface>>
+ getTrataArquivo() : TrataArquivo
TrataArquivo
getTrataArquivo obtém nome da
classe cuja instância é criada via LinuxFile WinFile MacOsFile
Class.forName(nome).newInstance().
Observe que não é imposta nenhuma restrição quanto à origem do nome da classe, que pode estar
armazenado em um arquivo properties, pode ser obtido via a moderna opção de configuração
fornecida via Preferences (pacote java.util.prefs), pode ser obtido de um banco de dados, de um
arquivo de configuração XML ou até mesmo de um URL específico, entre outras.
Exemplo
A pequena aplicação abaixo ilustra o emprego do padrão Factory. A classe TrataArquivoFactory
contém o método getObjectFromClass(String), que obtém um objeto da classe cujo nome é
fornecido como argumento e o método getClassName(), que é apenas uma abstração para a escolha
da classe que deverá ser criada. Conforme comentado anteriormente, esta decisão pode se basear em
uma lógica complexa, ou pode a classe desejada simplesmente ser fornecida em um arquivo. Neste
exemplo, por diversão, usouse um sorteio. Em um cenário mais realista, o sorteio do método
getClassName() seria substituído por uma identificação do sistema operacional em execução.
package factory;
interface TrataArquivo {}
// Factory Method
public static TrataArquivo getTrataArquivo() {
return (TrataArquivo) getObjectFromClass(getClassName());
}
5 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
} catch (Exception e) {
return null;
}
}
}
Execuções deste programa deverão produzir, na saída padrão, os nomes das classes que foram
sorteados e cujas instâncias correspondentes foram empregadas para se obter os nomes impressos.
Exemplo
Um outro exemplo pode ser fornecido quando a criação de uma instância é realizada por meio de um
classe armazenada em um array de bytes. Em cenário como este, a classe pode estar disponível em
um registro de um SGBD, por exemplo, cujo nome não é conhecido senão em tempo de execução, ao
contrário do exemplo anterior. Para este exemplo, defina Servico a interface que contém operação de
interesse, conforme abaixo.
package factory;
Para usufruir de uma implementação desta interface é necessário requisitar a operação correspondente,
conforme abaixo. Observe, contudo, que não é feita nenhuma referência para a classe que implementa
a interface. Não há nenhuma indicação de quem, de fato, executará a operação desejada. O código
abaixo limitase a obter uma instância por meio de ServicoFactory (emprego do padrão Factory
para a interface Servico) e envio da mensagem de interesse.
package factory;
A execução da operação desejada será fornecida pelo retorno do Factory que, portanto, deve conhecer
pelo menos uma implementação. O conhecimento de qual classe implementa a interface é fornecido
por um arquivo de propriedades. Para este exemplo, este arquivo deve conter uma única linha,
conforme abaixo, onde é indicado o nome do arquivo que contém a classe que implementa a interface
Servico.
servico=ImplementaServico.class
O que se observa acima é a identificação do nome de um arquivo, por meio da chave identifica por
servico, que implementa a interface Servico. Para recuperar este arquivo de propriedades
(configuracao.properties) e obter o valor da chave servico podese fazer uso da classe
Properties. A implementação do Factory, fornecida abaixo, fornece os detalhes.
package factory;
import java.util.Properties;
public class ServicoFactory extends ClassLoader {
private static ServicoFactory sf = null;
6 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Servico s = null;
Properties p = new Properties();
try {
p.load(getSystemResourceAsStream(origem));
String cls = p.getProperty("servico");
byte[] bytes = new byte[2048];
int nb = getSystemResourceAsStream(cls).read(bytes);
Class classe = sf.defineClass(null, bytes, 0, nb);
s = (Servico) classe.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return s;
}
}
No código acima, logo após obter o nome do arquivo contendo a classe que implementa a interface
Servico, um array de 2k bytes é criado. Este array é criado para armazenar todo o conteúdo do
arquivo contendo a classe que implementa a interface. Naturalmente este passo não é necessário, mas
foi fornecido para ilustrar que, de fato, a implementação da interface pode vir, inclusive, de um
SGBD. Neste caso, uma consulta seria realizada e a seqüência de bytes devidamente obtida. Uma vez
de posse da seqüência de bytes, o processo que segue é o mesmo, independente de como foram
obtidos. Em particular, o método protegido defineClass é chamado (isto justifica porque este
Factory estende a classe ClassLoader. A partir do ponto que se tem uma instância de Class
correspondente à classe de interesse, o passo seguinte é requisitar a criação de uma instância, o que é
possível por meio da mensagem newInstance().
Embora o código deste Factory seja relativamente simples, isto não impede que seja ilustrado o
processo de criação de uma instância de forma “elaborada”, possivelmente obtida de classe
armazenada em um SGBD, conforme comentado acima. Em conseqüência, as operações necessárias
para se criar instâncias assim, conforme ilustrado no código acima, devem estar em uma classe que
implementa o padrão Factory.
Exemplo
Um último e simples exemplo é fornecido abaixo. A classe Utils contém o método newDate(), que
retorna uma instância de java.util.Date conforme o dia, mês e ano fornecidos como argumento.
public class Utils {
public static Date newDate(int dia, int mes, int ano) {
Calendar c = Calendar.getInstance();
c.set(dia, mes, ano);
return c.getTime();
}
}
Exercícios
Para a solução de cada um dos itens abaixo escreva uma ou mais classes em Java com funcionalidade
suficiente para implementar todos os requisitos fornecidos.
1. O exemplo acima obtém a classe a ser criada de um sorteio. Em cenário real é bem provável que
este não seja o caso. Neste exercícios, em vez de um sorteio, faça uso do padrão Factory que
identifica qual a classe a ser criada de um arquivo de propriedades. A classe
java.util.Properties oferece os serviços load e store (argumentos omitidos), que podem ser
7 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
empregados para carregar o conteúdo de um arquivo de propriedades e, no sentido inverso, para
gravar as propriedades em um arquivo. Há variantes destes métodos para tratar arquivos XML
através dos métodos storeToXML e loadFromXML. Para obter o valor de uma propriedade há o
método getProperty(String key), onde key representa o identificador da propriedade. O
método setProperty(String key, String value) que cria a propriedade key com o valor
value.
2. A classe ImpostoDeRenda possui o método calculo() que utiliza um de dois métodos para
cálculo do imposto de renda: ou o simplificado ou completo, implementados pelas classes
Simplificado e Completo, respectivamente. Estas duas classes são derivadas da classe
MetodoCalculo, que é abstrata e possui um único método que, à semelhança da classe
ImpostoDeRenda, também é calculo(). O método calculo() da classe ImpostoDeRenda deve
fazer uso de uma implementação de MetodoCalculo, sem exatamente conhecer quem implementa
o método, ou seja, se a classe Simplificado ou a classe Completo. A decisão de qual método
empregar é definida em tempo de execução, quando uma instância da classe
ImpostoSimplificado ou da classe ImpostoCompleto, ambas derivadas de ImpostoDeRenda, é
criada. Código cliente da classe ImpostoDeRenda deverá empregar uma instância de uma das
classes que implementa ImpostoDeRenda sem conhecer realmente quem implementa esta classe
abstrata. A decisão de qual classe utilizar é decidida no método newImpostoDeRenda() da classe
ImpostoDeRendaFactory. Este método static retorna uma instância de ImpostoDeRenda.
Estabeleça a decisão de qual classe empregar da forma que considerar apropriado. Observe que a
instância retornada deverá estar apta a receber a mensagem calculo() cuja operação deverá ser
consistente com a classe da instância criada.
Implementação
A implementação deste padrão envolve o emprego de várias classes, em um cenário relativamente
complexo de interação entre as classes. Suponha a existência de uma classe Data e a classe Moeda. A
primeira representa uma data qualquer, por exemplo, 7 de setembro de 2004. A outra representa uma
8 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
quantidade em dinheiro, por exemplo, R$25,00. Em ambos os casos você deve ter observado que a
forma empregada para exibir a data e a quantia em dinheiro são típicas do Brasil. Para um país de
língua inglesa outra forma deveria ser utilizada. Ou seja, September 07, 2004 para a data e $25.00
para a moeda (estou assumindo a paridade de um dólar para um real).
O que é preciso neste caso, é a implementação das classes Data e Moeda conforme a localidade. Para
o Brasil, por exemplo, temse implementações distintas daquelas para países de língua inglesa,
conforme a figura abaixo.
Para uma aplicação cliente, o padrão Abstract Factory pode ser empregado para evitar que a aplicação
use, por exemplo, a implementação para o Brasil da classe Data e a implementação para um país de
língua inglesa para a classe Moeda, misturando data de um com moeda de outro. O que é preciso,
neste caso, é recuperar instâncias de classes correlatas.
Uma classe que implemente os métodos getMoeda e getData podem identificar a localidade, através
de um teste, por exemplo, e retornar a instância da classe identificada. Contudo, esta abordagem é
inapropriada, principalmente à medida que o número de opções crescer. Dois países não oferece
tantos desafios, mas não podemos dizer o mesmo para centenas deles. A manutenção seria dificultada,
mesmo todo este código estando devidamente confinado em uma única classe.
A implementação dos métodos getMoeda e getData é trivial, simplesmente retornando uma instância
da classe MoedaBrasil e DataBrasil, respectivamente. Naturalmente, MoedaBrasil herda da classe
Moeda e DataBrasil herda da classe Data. Estas classes de sufixo Brasil, convém relembrar, devem
implementar o formato empregado no Brasil para a exibição de valores monetários e de datas. Algo
similar seria obtido com outra localidade qualquer, conforme ilustra a figura.
9 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
A solução apresentada no diagrama acima faz uso do padrão Abstract Factory. Para a aplicação
cliente, convém ressaltar, há dependências para as classes LocalidadeAbstractFactory, Moeda e
Data. O cliente depende de LocalidadeAbstractFactory porque é a partir desta que as instâncias
adequadas de Moeda e Data são obtidas. Naturalmente, código cliente depende das classes Moeda e
Data, pois são estas que são efetivamente fornecem a funcionalidade desejada. Obserque que o código
cliente, ao fazer uso de uma instância de Moeda não sabe se se trata de uma instância da classe
MoedaBrasil ou MoedaEUA, que foi retornada por uma classe derivada de
LocalidadeAbstractFactory, também desconhecida da aplicação.
Exercícios
Implemente o modelo comentado acima, onde as seguintes restrições deverão ser satisfeitas.
1. Uma classe Cliente deverá representar código cliente.
2. Crie a classe Factory a partir da qual o método newLocalidade() deverá retornar uma instância
de LocalidadeAbstractFactory. A decisão deverá vir da configuração de um arquivo de
propriedades, conhecido pela classe Factory. A propriedade localidade deverá indicar Brasil para
a localidade do Brasil ou EUA para a localidade dos EUA.
3. A classe Cliente, ao fazer uso de Data e Moeda, não deverá conhecer a implementação da classe
LocalidadeAbstractFactory assim como também não conhecerá quem implementa Data e
Moeda.
10 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
8. Para formatar o dia da semana de uma data para determinado Locale pode ser empregada a classe
java.text.SimpleDateFormat. Exemplo: SimpleDateFormat(“E”, Locale.US). Cria uma
instância de DateFormat para a qual a mensagem format(new Date()) oferece o resultado
esperado. Consulte a documentação da biblioteca Java para detalhes dos argumentos.
Builder (criação)
Em alguns cenários é desejável a criação de objetos distintos, mas cujos processos de criação são
similares. Objetos que são construídos em vários passos beneficiamse deste padrão. Demais padrões
de construção enfatizam a criação de um objeto em um único passo. Este padrão envolve dois
elementos fundamentais: o Builder e o Director.
O Builder define as operações para a criação de partes do objeto que se deseja construir. São
implementações da interface ou classe abstrata Builder os únicos que sabem como combinar partes do
objeto que se deseja construir.
Cada implementação sabe como criar um tipo de objeto específico, mas não possui as partes, que são
fornecidas pelo Director. O Director, sem conhecer os detalhes da como combinar as partes, mas
tendo acesso a elas, as repassa para uma implementação do Builder.
O objetivo deste padrão, segundo a GoF, é separar a construção de objetos complexos das respectivas
representações, de tal forma que o mesmo processo de construção possa criar diferentes objetos.
Em resumo, o Director sabe como construir um objeto, ou melhor, uma família de objetos cujos
processos de construção são semelhantes, além de possuir acesso às partes dos objetos que devem ser
construídos. É de um Builder a responsabilidade por reunir estas partes. Cada Builder dá origem a
uma representação distinta (objeto distinto).
A GoF fornece um exemplo onde o Director é um leitor de arquivo RTF e, sem conhecer como um
arquivo RTF pode ser convertido em uma versão TXT correspondente, cada parte do arquivo RTF
lido é fornecido a um Builder que implementa a representação TXT. Ao final da leitura do arquivo
RTF o Director terá requisitado ao Builder a construção do objeto TXT correspondente ao arquivo
RTF lido. Observe que o emprego do padrão Builder permite, neste exemplo, o acréscimo de novas
representações de um arquivo RTF, ou seja, novos formatos para os quais um arquivo RTF pode ser
convertido sem que o leitor de RTF, o Director neste exemplo, tenha conhecimento ou precise ser
modificado para viabilizar esta conversão.
Em outro cenário, uma aplicação pode empregar várias representações de um volante da loteria
esportiva. Uma delas, por exemplo, pode ser adequada para cálculos estatísticos, enquanto outra mais
adequada para a simples impressão do volante. Naturalmente, qualquer que seja a representação
empregada, o processo de criação de um volante é o mesmo e, portanto, o padrão Builer pode ser
utilizado para a construções destes objetos, onde teríamos um Builder para cada “formato” final
desejado e o Director para “ler” os volantes.
O livro Thinking in Patterns, Bruce Eckel, disponível na web, oferece um exemplo que é apresentado
abaixo com pequenas modificações. Neste exemplo temse o interesse na confecção de três objetos
distintos, Livro, CDROM e Revista, mas cujos processos de criação são similares e, por conseguinte,
abstratamente capturados pela classe abstrata MidiaBuilder. Para cada um destes objetos há um
Builder correspondente, conforme ilustra a figura.
Todos os objetos são derivados da classe Midia. Ao receber um Builder sobre o qual o Director fará
requisições, este saberá que o resultado final será uma instância derivada da classe Midia
correspondente ao Builder recebido. Convém ressaltar que o Director, neste exemplo MidiaDirector
conforme abaixo, atuará sobre um MidiaBuilder e, a rigor, não precisa conhecer a classe concreta
que implementa o Builder desejado.
11 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Exercícios
Faça código que implementa o padrão Builder para tratar o cenário onde vários tipos distintos de
relatórios devem ser gerados por uma empresa. Detalhes seguem nos parágrafos seguintes e
empregam a nomenclatura introduzida nesta seção. Tenha a liberdade para implementar, da forma que
considerar mais apropriada, o que não estiver prescrito abaixo.
O Director, implementado pela classe RelatorioDirector, é responsável por percorrer uma lista
onde cada elemento da lista é um array contendo exatamente dois valores do tipo int. Cada array é
transferido para um Builder através da chamada ao método processaArray(int[]) da classe
abstrata RelatorioBuilder. Ou seja, a função do Director é percorrer a lista e, para cada elemento
desta, enviar a mensagem processaArray para uma instância de RelatorioBuilder. Observe que o
Director desconhece o que exatamente cada Builder faz em reação à mensagem enviada.
Neste exercício três Builders deverão ser implementados. Um para um relatório simples, um para um
relatório denominado soma e o terceiro para o relatório denominado de acumulado. Cada um destes
relatórios são detalhados abaixo. Cada tipo de relatório possui a sua própria classe e o seu próprio
Builder. Cada um destes Builders, em reação à mensagem processaArray irá realizar uma operação
compatível com o tipo de relatório correspondente.
Cada classe que representa um tipo de relatório herda da classe Relatorio. Para este exercício, o
único método relevante da classe Relatorio é o método toString().
O relatório simples será criado pelo Builder SimplesBuilder. Uma instância desta classe, à medida
que recebe mensagens processaArray, simplesmente monta uma lista análoga àquela da instância de
RelatorioDirector. Ou seja, cada array será copiado e a cópia acumulada em uma outra lista, a
lista que é propriedade da classe RelatorioSimples. SimplesBuilder cria uma instância de
RelatorioSimples, que é uma classe derivada de Relatorio. Ao receber a mensagem toString(),
uma instância de RelatorioSimples exibe o conteúdo da lista que retém. Cada entrada da lista
deverá ser impressa em uma única linha. Os dois valores do tipo int de cada entrada deverão ser
impressos separados por uma vírgula e um espaço, conforme exemplificado abaixo, para uma
hipotécia lista contendo apenas duas entradas. A primeira com o array correspondente contendo os
valores 1 e 2 e a segunda com o array contendo os valores 3 e 4.
1, 2
3, 4
O relatório soma é implementado pela classe RelatorioSoma. Uma instância de RelatorioSoma é
criado pela classe SomaBuilder. RelatorioSoma, à semelhança da classe SimplesBuilder, também
contém como atributo uma lista. Cada elemento desta lista é um valor do tipo int, obtido da soma de
cada array fornecido ao SomaBuilder.
O relatório acumulado, uma instância de RelatorioAcumulado, também é derivada da classe
Relatorio. Uma instância de RelatorioAcumulado, contudo, tem apenas uma propriedade
relevante, total, do tipo int. Esta propriedade deverá conter a soma de todos os valores passados
para o AcumuladoBuilder.
12 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Deverão ser gerados, conforme detalhes acima, pelo menos 10 classes. São três classes
correspondentes aos Builders: SimplesBuilder, SomaBuilder e AcumuladoBuilder. Outras três
classes correspondentes aos tipos de relatórios: RelatorioSimples, RelatorioSoma e
RelatorioAcumulado. Isto perfaz seis classes. Uma classe ancestral de todas estas três últimas,
Relatorio e uma classe abstrata ancestral de todos os Builders, RelatorioBuilder. Também será
necessária a classe RelatorioDirector que, para este exercício, possui uma lista contendo arrays,
cada um deles contendo exatamente dois inteiros e uma classe RelatorioTest (teste de unidade
empregando o JUnit). Esta última deverá conter exatamente três métodos: testeCasoSimples,
testCasoSoma e testCasoAcumulado. Cada um destes métodos deverá possuir um assert
apropriado para validar o retorno da mensagem toString() quando enviada para uma instância de
tipo correspondente.
Prototype (criação)
A criação de instâncias de algumas classes pode exigir considerável consumo de recursos. De forma
mais técnica, a execução do construtor pode ser “lenta”, exigir “muita” memória e outros. Nestes
casos pode ser útil o emprego do padrão Prototype, que visa, em vez de criar uma nova instância de
uma classe, realizar uma cópia de uma instância já existente. Desta forma, o ônus da criação é
substituído pelo ônus da cópia, cuja criação pode ser bem mais eficiente que a criação de um novo
objeto.
Um objeto que representa o resultado de uma consulta a todos os clientes de uma empresa que
compraram produtos no último mês pode retornar uma considerável quantidade de informação.
Imagine que seja necessário percorrer estes clientes conforme a ordem de compra. Neste caso, o
objeto pode ser gerado nesta ordem. Contudo, se desejarmos classificar os clientes em ordem
alfabética, ao realizarmos esta operação sobre o objeto resultante, a ordem anterior é destruída. Criar
um novo objeto para que esta nova classificação possa ser empregada sem destruir a anterior é um
processo oneroso. Neste caso, a solução é criar uma cópia do objeto obtido com a primeira consulta, o
que resultaria em dois objetos, com o mesmo conteúdo mas custo de criação bem diferentes.
Clones em Java
A classe Object em Java contém o método protected Object clone(). Ao receber a mensagem
close(), o resultado esperado é uma cópia do objeto que a recebe. Cada classe deve implementar a
sua própria concepção de cópia. Também é necessário que a classe implemente a interface
Cloneable. Estas restrições, juntamente com o sentimento de que é muito mais seguro e fácil fazer
uso de um construtor que recebe como argumento um objeto da classe em questão, torna o emprego
deste método desaconselhável por alguns.
Adicione às questões apresentadas o fato do método clone() realizar o que é conhecido por cópia
rasa ou parcial (shallow copy). Ou seja, as referências são copiadas, mas não os dados subjacentes.
Isto significa que uma mudança em um objeto referenciado por uma cópia irá repercutir igualmente no
objeto original e viceversa. Em alguns casos isto pode ser indesejável e uma cópia total (deep copy)
deve ser empregada. O método abaixo pode ser empregado em uma classe para permitir a cópia total
de um objeto, ou seja, todas as referências e os dados correspondentes são copiados. O método exige
que o objeto fornecido como argumento implemente a interface java.io.Serializable. Esta
exigência é da classe ObjectOutputStream.
ByteArrayInputStream bIn;
13 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Observe que este método não copia apenas o objeto fornecido mas todo o grafo que pode ser obtido a
partir deste objeto. Ou seja, se o objeto fornecido como argumento possui uma referência para outro
objeto e este para outro e assim por diante, todos eles serão copiados. Naturalmente, se se deseja
copiar apenas o objeto fornecido como argumento e não os objetos que podem ser obtidos a partir
deste, então será necessário empregar outro método para a cópia.
Exemplo
A classe Ponto, fornecida abaixo sobrescreve o método clone() herdado de Object. Observe a
forma simples empregada pela classe para se obter uma nova instância com base em uma existente.
Ao contrário do método deepCopy(), comentado anteriormente, a implementação de clone()
fornecida abaixo faz uso do construtor de Ponto.
package prototype;
Exercícios
1. Crie código para exercitar o uso do método clone().
2. Faça uma implementação da abordagem deep copy e realize testes para constatar que a cópia está
sendo realizada da forma esperada.
Singleton (criação)
Este padrão é empregado para assegurar que apenas uma instância de determinada classe será criada
por aplicação. Há aplicações que devem fazer uso de uma única instância para acesso a informações
persistentes, por exemplo. Nestes casos, a existência de mais de um objeto pode comprometer a
integridade das informações.
O padrão Singleton também pode ser empregado para oferecer um ponto de acesso comum a um
objeto. Por exemplo, objetos Factory (3), geralmente são empregados em conjunto com o padrão
Singleton, de tal forma que exista um único objeto Factory para cada finalidade desejada e um único
14 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
ponto de acesso ao mesmo. Entenda um único ponto de acesso como visibilidade global, onde
qualquer parte da aplicação pode ter acesso ao objeto desejado.
A figura abaixo ilustra a aplicação deste padrão na classe Singleton. Observe o emprego da
propriedade instance, declarada privada; do construtor declarado protected e do método
getInstance(). Embora não seja uma exigência, é comum o emprego do método getInstance para
se obter uma instância da classe que recebe esta mensagem.
Esta é uma das três opções de implementação deste padrão. O método getInstance() é chamado
para retornar a instância desejada. Em conseqüência, este método deve ser declarado público e
static, o que assegura o acesso à instância de interesse onde quer que a classe Singleton seja
visível, pois onde esta for visível será possível enviar esta mensagem.
Singleton
instance : Singleton
# Singleton()
+ getInstance() : Singleton
Uma implementação induzida pelo modelo acima é fornecido a seguir. Observe que o código tem
propósito didático e, portanto, as operações “reais” que estariam presentes foram abstratamente
representadas pelo método operation().
protected Singleton () {
}
Opções de implementação
A classe Impressora apresenta outra proposta de implementação do padrão Singleton. Nesta versão,
uma variável lógica indica se a instância desejada já foi ou não criada. Quando se tenta criar uma
instância desta classe, o construtor consulta a variável. Se pelo menos uma instância já foi criada e se
encontra em “uso”, então uma exceção é gerada. Caso contrário, o construtor é executado
normalmete, o que oferece a instância desejada para quem tenta criar a instância.
class Impressora {
static boolean existeInstancia = false;
15 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
}
}
public SingletonException(String s) {
super(s);
}
}
Esta proposta, embora assegure a existência de uma única instância, apresenta uma responsabilidade
adicional ao programador, que será responsável pela chamada ao método fim() para indicar que a
instância em questão não mais será empregada. Naturalmente, esta exigência pode estar além da
disciplina da equipe de programação e, portanto, suscetível a problemas.
Uma terceira versão é fornecida abaixo.
package singleton;
private Teclado() {
}
Exercícios
1. Crie uma classe, por exemplo, Biblioteca, e assegure que aplicações que farão uso desta classe
só terão acesso a no máximo uma instância desta classe por processo.
2. Estabeleça comparações entre as versões 1, 2 e 3 para a implementação do padrão Singleton.
Quais as vantagens de um em relação ao outro.
3. Escreva uma aplicação que, em instantes distintos da sua execução, faça uso de duas instâncias
distintas de Impressora. Sugestão: após criar uma instância, não mais retenha uma referência
para o objeto criado e “tente” forçar a execução do garbage collector (por exemplo, via chamada
a System.gc()).
4. Discuta a possibilidade ou não de fazer o mesmo do item anterior para a classe Singleton.
5. Modifique a classe Singleton para que sempre, mesmo que uma instância não seja utilizada,
esta se encontre à disposição.
6. O trecho de código abaixo foi extraído de um programa cujo autor diz que mostra uma falha na
implementação da classe Singleton, fornecida neste texto, pois permite o emprego de duas
instâncias simultâneas onde só poderia existir uma. Verifique a afirmação deste hacker.
public static void main(String[] args) {
16 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Singleton s = getInstance();
QuebraSingleton qs = new QuebraSingleton();
7. Crie uma variante do padrão Singleton que permite a criação de um número específico de
instâncias em vez de apenas uma. Este padrão também é conhecido por Object Pool.
8. A biblioteca Java possui uma implementação do padrão Singleton não comentada anteriormente.
Uma classe declarada final cujos métodos são todos public e o construtor é private também
é uma forma de implementar o padrão Singleton, embora aplicável em casos específicos. Qual é
a classe que atende a tais características?
9. O que ocorre quando se tenta criar uma instância de uma subclasse de um Singleton que já possui
uma instância?
10. As opções de implementação apresentadas acima não contemplam a execução concorrente
(multithread). O que é necessário modificar para viabilizar as opções acima em um programa
concorrente?
Adapter (estrutura)
Nas relações internacionais do Brasil com a China é necessária a existência de intérpretes, pelo menos
em alguns casos. Um interlocutor, nestes casos, é responsável pela comunicação
entre partes que fazem uso de línguas bem distintas. Apesar das diferenças, a
comunicação deve ser estabelecida. O amigo chinês não se fará entender sem o
intérprete, assim como o brasileiro. De forma análoga, um aparelho que opera em
110V exige um transformador para uma tomada 220V. Todos eles são “adaptadores”, assim como o
objeto ao lado.
Imagine o cenário em que uma aplicação precisa ser freqüentemente alterada para contemplar as
novas versões de uma classe amplamente empregada, onde cada nova versão apresenta diferenças em
relação a anterior. Por exemplo, suponha que em uma das mudanças a classe OfereceServico possui
o método fazAlgumaCoisa(), que foi substituído por fazOutraCoisa() em versão posterior.
Também suponha que foi removido o argumento do método faz(String), o que resulta na versão
faz(), onde o argumento deverá ser fornecido através da chamada a um novo método,
setString(String), antes que faz() seja chamado. Esta mudança é ilustrada na figura abaixo,
contendo a versão resultante e aquela anterior.
OfereceServico OfereceServico
Versão antes Versão após
de mudança. + fazOutraCoisa() mudança.
+ fazAlgumaCoisa()
+ setString(String)
+ faz(String)
+ faz()
Este cenário sugere mudanças na aplicação para que a nova versão possa ser utilizada sem problemas.
Ou seja, quando OfereceServico se altera, código cliente desta classe também terá que ser alterado,
conforme ilustra a figura abaixo. Noutras palavras, há dependência entre a aplicação (cliente) e a
classe OfereceServico. Não há nada inerentemente errado com a dependência entre duas classes,
exceto quando a classe dependida é instável e nenhum mecanismo de proteção, como o padrão
Adapter, é empregado.
17 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Sempre que esta classe for
Abstração de código da alterada, Cliente deverá ser
'aplicação' que faz uso investigado à procura de
de OfereceServico. mudanças correspondentes.
Cliente OfereceServico
Felizmente o padrão Adapter pode ser empregado para proteger a aplicação das variações da classe
OfereceServico. Na presença de um adaptador, mudanças na classe OfereceServico poderão
repercutir, no máximo, no adaptador, o que protege qualquer cliente de possíveis alterações. Como
isto pode ser feito?
Os serviços requisitados por um cliente podem ser encapsulados em uma interface. Desta forma, o
cliente não depende de uma implementação específica, mas apenas da interface, conforme é ilustrada
o diagrama abaixo. Neste caso, qualquer que seja a implementação, desde que a interface não seja
alterada, a classe Cliente não será afetada.
Depende das operações definidas na Implementa operações
Interface. Desconhece a existência de da Interface.
OfereceServico.
Cliente OfereceServico
Interface
Enquanto para alguns casos o simples uso de uma interface pode ser suficiente, há aqueles onde uma
abordagem mais elaborada fazse necessária. Afinal, o emprego de uma interface restringe
repercussões na classe Cliente e transfere todo o ônus de alterações para a classe que implementa a
interface.
No cenário de mudança comentado acima, por exemplo, ao alterar o nome do método
fazAlgumaCoisa() por fazOutraCoisa(), teríamos que manter ambos os métodos na classe
OfereceServico, pois a classe Cliente fará uso do método fazAlgumaCoisa(). Assuma que os
serviços desejados são aqueles presentes na versão antiga, ou seja, aquela antes da alteração. Em
conseqüência, OfereceServico terá que mantér este método e acrescentar fazAlgumaCoisa()
conforme o código abaixo.
Observe que, desta forma, não teríamos de fato uma “nova” versão da classe OfereceServico, mas
uma classe que mantém a compatibilidade com a versão anterior e, ao mesmo tempo, acomoda
mudanças. Contudo, esta solução só é possível quando se tem controle sobre a nova versão da classe.
Nem sempre isto ocorre. O fornecedor desta classe pode não estar disposto a manter a compatibilidade
com versões anteriores e simplesmente renomear o método. O modelo abaixo ilustra o emprego do
padrão Adapter para sanar este problema.
18 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
public class Adaptador implements Interface {
OfereceServico adaptado;
A classe Adaptador encapsula
...
mudanças em OfereceServico,
public void fazAlgumaCoisa() {
tornandoas invisíveis às classes que
fazOutraCoisa();
fazem uso de Interface.
}
...
}
<<Interface>>
Cliente Interface Adaptador
+ fazAlgumaCoisa()
adaptado Em versão anterior possuía
fazAlgumaCoisa(), cujo
Em algum método desta classe, OfereceServico nome foi alterado para
onde obj é referência para instância fazOutraCoisa() sem
de Adaptador, vêse ... repercussão em Cliente.
obj.fazAlgumaCoisa();
19 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
public Adaptador() {
adaptado = new OfereceServico();
}
Também observe a versão resultante das mudanças naquela anterior.
Sem o emprego do padrão Adapter, teríamos que fazer mudanças correspondentes na classe Cliente
conforme ilustrado abaixo, pois a interface empregada seria alterada. Observe que a alteração é
simples devido ao emprego trivial que é feito. Em casos reais provavelmente mais de uma classe
estaria fazendo uso da interface, assim como este uso estaria distribuído, provavelmente, por vários
métodos.
20 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Sem o emprego do padrão Adapter
Versão antes da alteração na interface Versão após alteração na interface
public class Cliente { public class Cliente {
Interface obj = null; Interface obj = null;
Com o emprego do padrão Adapter, a versão à esquerda na tabela acima não precisa sofrer alterações
quanto a classe OfereceServico é alterada. Conforme comentado anteriormente, as mudanças são
encapsuladas em Adaptador, que protege a classe Cliente de variações na classe OfereceServico.
A tabela abaixo mostra a alteração na classe Adaptador necessária para que a classe Cliente seja
mantida intacta, enquanto a classe OfereceServico sofre alterações. Observe ainda que a classe
Adaptador é apenas um intermediário, em ambos os casos. Quem executa os serviços nas duas
versões é a classe OfereceServico.
Emprego do padrão Adapter
Adaptador antes da alteração em OfereceServico Adaptador após da alteração em OfereceServico
public class Adaptador implements Interface { public class Adaptador implements Interface {
OfereceServico adaptado = null; OfereceServico adaptado = null;
21 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Versão V1 Versão V2
Para representar a nossa aplicação é apresentada a classe abaixo, responsável por criar duas instâncias
da classe Cliente, cada uma delas fará uso de uma implementação distinta da interface utilizada.
Para que não fique dúvidas acerca das versões distintas, observe que as implementações de
OfereceServicoV1 e OfereceServicoV2 acrescentam um prefixo antes de exibir a String
requisitada.
c1.faz(“um teste”);
c2.faz(“um teste”);
}
}
A execução do código acima faz uso de uma única classe Cliente cuja instância referenciada por
c1 comportase diferente daquela referenciada por c2. De fato, podemos ter comportamentos tão
distintos quanto desejarmos sem modificações correspondentes na classe Cliente, desde que seja
mantida a interface. Isto permite, conforme já foi afirmado anteriormente, a independência entre a
classe Cliente e a implementação dos serviços por esta utilizados.
22 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
MOTIVAÇÃO. Convém ressaltar que este é apenas um exemplo didático onde a alteração efetuada é
mínima, assim como a repercussão desta. Em casos reais, contudo, podemos ter dezenas, talvez
centenas ou mais classes dependentes que podem exigir alterações correspondentes. Alguns podem
dizer que toda a mudança pode ser efetuada com uma “substituição global de texto por outro”.
Francamente falando, não conheço ninguém que não se sinta desconfortável com esta tarefa perante
um sistema de centenas de milhares de linhas de código. Será necessário recompilar todas as
mudanças e realizar testes. Se isto não é motivação suficiente, talvez seja o fato de que a classe
Cliente e a classe OfereceServico possam ser fornecidas por empresas distintas, onde uma não
tem acesso ao código da outra e viceversa. Neste cenário, uma “substituição global de texto por
outro”, por menor e mais simples que seja, não é viável.
Ou seja, quando uma instância de Cliente requisita a execução de uma operação definida na
Interface, será da responsabilidade da instância de Adaptador a execução requisitada. A classe
Adaptador, por sua vez, como o próprio nome indica, funciona apenas como intermediário ao
traduzir as requisições da instância de Cliente em chamadas a instância empregada de
OfereceServico. O diagrama de seqüência abaixo ilustra uma interação típica entre instâncias destas
classes.
Enviada por instância de Cliente sem conhecimento da
instância de Interface que irá recebêla.
1: fazAlgumaCoisa( )
2: fazOutraCoisa()
Exercícios
1. O método substring(int beginIndex, int endIndex) da classe java.lang.String tem
um comportamento peculiar quanto aos argumentos. O argumento beginIndex indica a posição
de início na String em questão a partir da qual uma subseqüência será obtida. O fim não é
fornecido por endIndex, mas por endIndex – 1. Ou seja, se você deseja obter a seqüência que
se inicia no primero caractere e termina no segundo, terá que fornecer como argumentos 0 e 2.
Naturalmente isto pode confundir muita programador. Supondo que a classe java.lang.String
é extensivamente utilizada por um sistema e que é muito freqüente o erro associado aos
argumentos de substring, o que poderia ser feito para amenizar o problema? (Dica: crie a sua
classe String que irá funcionar como um adaptador. Esta classe deverá conter os métodos
empregados da classe java.lang.String. Provavelmente terá que fazer uma substituição
global onde java.lang.String seja substituída por br.ufg.inf.meuprojeto.String, por
exemplo. A implementação da sua classe fará uso de java.lang.String. Quando a mensagem
substring for recebida está será transmitida para a instância de java.lang.String
correspondente com uma pequena alteração no argumento da seguinte forma: substring(x,y)
resultará na chamada a substring(x,y+1).)
Bridge (estrutura)
Desenvolver software orientado a objeto envolve, freqüentemente, o emprego de herança, onde uma
subclasse encapsula um caso particular como a implementação de um serviço para determinado
cenário. Por exemplo, a classe ImpressoraX herda da classe Impressora alguns serviços enquanto
outros são sobrescritos como imprime(String), que exige uma implementação distinta para a classe
23 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Se desejarmos uma implementação do serviço adequada para a exibição em monitores pequenos
como aqueles de dispositivos móveis, provavelmente terminaremos com duas subclasses para
Requisição e outras duas para Comprovante. Cada uma delas tratando especificamente cada um dos
cenários apresentados, conforme o diagrama abaixo ilustra. Foram empregados os sufixos Padrão e
Menor, respectivamente, para representar o monitor convencional e aquele de dimensões reduzidas.
Documento
+ formatar()
Requisição Comprovante
Embora o modelo acima implemente os requisitos apresentados, observase facilmente que para
cada tipo de documento teremos duas subclasses. O cenário é ainda menos favorável se imaginarmos
o surgimento de um novo tipo de monitor, pois teríamos um crescimento significativo do número de
classes e, com certeza, uma dificuldade na manutenção delas. Felizmente, os inconvenientes deste
cenário podem ser eliminados com o emprego do padrão Bridge.
O que chama a atenção nesta hierarquia é que residem nela tanto a abstração de documentos quanto
as implementações correspondentes. O padrão Bridge sugere que a abstração, ou seja, as classes
Documento, Requsição e Comprovante sejam mantidas em uma hierarquia distinta daquela da
implementação, onde residem as classes Formatação, Padrão e Menor, conforme ilustra o diagrama
abaixo.
24 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
toString() {
return formatar(str);
}
Observe que a hierarquia anterior foi fragmentada em duas: uma contendo as abstrações de
documentos, relevante para a classe Cliente, e outra contendo a implementação destas abstrações.
Neste exemplo, a implementação resumese ao serviço de formatar documentos para exibição em dois
tipos de monitores. No futuro, caso novas formas de formatação sejam criadas, a inserção não irá
interferir nos clientes da classe Documento, assim como também não provocarão um crescimento
significativo do número de classes e a decorrente dificuldade de manutenção.
Convém ressaltar que a classe Documento e suas subclasses não terão que se preocupar com a
formatação para dispositivos específicos, pois este conhecimento foi isolado na hierarquia da direita.
O método toString(), conforme a nota, deverá chamar o método formatar() sem conhecer quem
estará oferecendo este serviço. No processo de construção de uma instância de uma subclasse de
Documento, o construtor pode receber como argumento a formataçao a ser empregada, isto é, uma
instância de Formatação, seja esta da classe Padrão ou Menor. O padrão Factory (3) pode ser
empregado para esta finalidade.
Outro exemplo. Podese interpretar estruturas de dados como listas, filas e árvores como abstrações
em uma hierarquia. Cada uma destas com sua respectiva classe e serviços, alguns herdados da classe
ancestral abstrata EstruturaDeDados. Seja ordena() um dos serviços definidos nesta classe.
Sabemos que vários são os métodos que podemos empregar para ordenação e, neste caso, o que se
deseja evitar, através do emprego do padrão Bridge, é a criação de classes como ListaBubleSort,
ListaQuickSort e assim por diante, cada uma delas, naturalmente, implementando ordena() com o
método correspondente ao sufixo empregado. O padrão Bridge sugere que criemos uma hierarquia
cuja classe no topo é Ordenação e subclasses QuickSort e BubleSort, por exemplo. Desta forma,
um novo método de ordenação que se fizer interessante para determinado contexto pode ser
empregado sem que tenhamos que criar tantas classes quanto o número de estruturas de dados criadas.
Não é difícil encontrar possibilidades de implementação do padrão Bridge, assim denominado por
estabelecer uma “ponte” entre as hierarquias comentadas anteriormente (aquela da abstração e aquela
da implementação).
Exercícios
Implemente código que satisfaz os seguintes itens.
1. Em casos mais simples pode ser suficiente a implementação da persistência de classes de negócio
ser fornecida na própria classe. Assim, a classe Pessoa, que herda de ClasseNegocio, assim
como a classe Carro, implementam o método abstrato persiste(), herdado de ClasseNegocio.
2. Faça uso do padrão Bridge para implementar a hierarquia de classes de negócio “isolada” da
implementação de persistência, que pode empregar um de dois métodos: XML e serialização. O
emprego do padrão Bridge, convém ressaltar, facilita o acréscimo de outros métodos, como o
emprego de um SGBD, por exemplo.
3. Não é necessário implementar efetivamente os métodos acima. Simule a persistência com uma
simples mensagem exibida na saída padrão indicando o método empregado seguido do conteúdo
do objeto a ser armazenado. Este conteúdo pode ser obtido simplesmente através do retorno do
método toString().
25 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Composite (estrutura)
Sistemas de informação manipulam coleções de objetos, muitas vezes compostas por elementos
simples e outras coleções. Por exemplo, um grupo pode ser formado por pessoas e outros grupos. Em
alguns casos é desejado um tratamento para o grupo semelhante ao de um elemento. Nestes cenários,
o padrão Composite oferece uma solução.
Um exemplo do padrão Composite é freqüentemente encontrado em editores gráficos. Nestes,
vários elementos gráficos como linha, retângulo, texto e outros podem ser agrupados, formando um
novo elemento. Este novo elemento, por sua vez, juntamente com outras linhas e retângulos, por
exemplo, podem ser agrupadas, dando origem a um novo agrupamento.
Chamemos cada um destes agrupamentos de elemento composto. Ou seja, uma agregação de
ElementoGráfico dá origem a uma instância de ElementoComposto, conforme figura abaixo.
ElementoGráfico elementos
+ desenhe() 2..*
A classe abstrata ElementoGráfico é uma abstração para todo tipo de elemento gráfico que pode ser
manipulado pelo editor do nosso exemplo. O diagrama ilustra apenas alguns dos tipos por
simplicidade: Texto, Círculo, Linha e ElementoComposto. ElementoComposto representa uma
agregação de elementos gráficos, pelo menos dois deles. Desta forma, quando o usuário “agrupar”
elementos devidamente selecionados, esta operação dará origem a uma instância de
ElementoComposto, cujas partes são os elementos selecionados que pode conter, inclusive, outros
elementos compostos.
A única operação fornecida em ElementoGráfico é desenhe(). Todo editor gráfico precisa pintar e
repintar os elementos que estão sendo editados, inclusive aqueles compostos. Dessa forma, quando
uma instância de ElementoComposto receber a mensagem desenhe(), será da responsabilidade desta
instância retransmitir esta mensagem para cada uma de suas partes. Isto ilustra o emprego do padrão
Composite.
O padrão Composite é formado por quatro participantes principais: um cliente, quem faz uso da
aplicação do padrão; classes concretas que são utilizadas pelo cliente, todas elas herdam de uma classe
abstrata; a classe abstrata que define as operações comuns a todas as partes (as classes concretas); e,
em particular, a classe concreta que agrega instâncias de classes derivadas da classe abstrata.
Tipicamente o cliente requisita uma operação para alguma instância de uma das classes concretas. O
cliente pode “desconhecer” o objeto que de fato recebe a mensagem. Isto é possível através de uma
referência para a classe abstrata. Qualquer que seja o objeto, contudo, seja ele um elemento não
composto ou composto por outros elementos, a operação será executada transparentemente da
perspectiva do cliente.
O padrão Composite também pode ser empregado para o registro de hierarquias. Por exemplo, uma
classe abstrata Membro herdada por duas outras classes, Pessoa e Grupo, onde Grupo é uma
agregação de Membro, é suficiente para o registro de um organograma. Na raiz teríamos uma empresa
(instância de Grupo) cujos membros seriam um presidente (instância de Pessoa) e departamentos
(instâncias de Membro), cada um destes departamentos por sua vez estariam organizados em outros
departamentos e pessoas até que tenhamos apenas pessoas, encerrando a hierarquia.
26 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Um outro emprego do padrão Composite é a descrição de estratégias (regras de negócio). Neste caso,
por exemplo, em uma empresa que oferece desconto, teríamos vários algoritmos encapsulados em
classes concretas, herdadas da classe DescontoStrategy. A instância a ser empregada para o cálculo
de desconto seria uma implementação desta classe em um cenário simples. Em outros, poderíamos ter
toda uma coleção de regras de desconto, mas onde apenas uma poderia ser aplicada. Esta coleção
pode ser implementada como uma agregação de DescontoStrategy.
Quando o cliente requisitasse o valor do desconto para um objeto composto por várias instâncias de
DescontoStrategy, esta instância poderia percorrer este conjunto de regras e selecionar aquela mais
vantajosa para o cliente. Assim poderíamos ter regras como “na compra de 2 lápis você ganha 5% de
desconto”, “se possui 60 anos ganha 10% de desconto nos produtos de limpeza” e assim por diante,
cada uma delas implementada em sua própria classe e selecionada por uma regra que é uma
composição destas.
Exercícios
Implemente código que satisfaz os seguintes itens.
1. O sistema de arquivo tanto no Linux quanto no Windows(r) são bem parecidos. Nestes, diretórios
contêm arquivos e/ou diretórios.
2. Trate arquivo como elemento simples (classe Arquivo) e diretório como elemento composto
(classe Diretorio) conforme o padrão Composite. Estas classes, ambas, devem herdar da classe
abstrata Elemento, que contém a operação long tamanho(). Esta operação retorna o tamanho em
bytes do arquivo ou diretório correspondente. No cado de um diretório, o tamanho é dado pela
soma dos tamanhos de todos os arquivos contidos no diretório e eventuais subdiretórios.
3. Crie um programa que receba como argumento um nome de um arquivo ou diretório. Por exemplo,
texto.txt ou /home/user/fulano. No primeiro caso o programa deverá retornar o tamanho em bytes
do arquivo fornecido. No segundo caso, o tamanho em bytes de todos os arquivos contidos no
diretório fornecido. O diretório pode conter subdiretórios e estes outros e assim por diante. Todos
os arquivos no diretório e eventuais subdiretórios devem ser computados. Se não for possível
“contar” o tamanho em bytes, então exiba a mensagem “Não foi possível computador o tamanho
de <arquivofornecidocomoarg>”. Isto pode ocorrer, por exemplo, por falta de permissão.
4. Deverá ser criada uma instância de Elemento correspondente ao argumento fornecido ao
programa através do emprego do padrão Factory, via classe ElementoFactory. Caso o usuário
forneça como argumento o nome de um arquivo, então uma única instância de Arquivo deverá ser
criada por ElementoFactory. Caso o usuário forneça um diretório, então uma instância de
Diretorio deverá ser criada. Uma instância de Diretorio deverá agrupar todas as instâncias de
Arquivo e/ou Diretorio que se fizerem necessárias para refletir a organização do diretório
fornecido. Em qualquer um dos casos, só após obtida a instância de Arquivo ou Diretorio
correspondente ao argumento fornecido, envie a mensagem tamanho().
5. Tanto um arquivo quanto um diretório, em Java, podem ser manipulados através de uma instância
da classe java.io.File. A forma new File(“arq.txt”) cria uma instância para o arquivo
fornecido como argumento enquanto new File(“/home/user/fulano”) para o diretório
fornecido. Se f é uma instância de java.io.File, sabese que f referese a um arquivo ou a um
diretório através dos métodos isFile() e isDirectory(), respectivamente. Ambos retornam um
valor do tipo boolean.
6. Dois métodos relevantes desta classe, para os interesses deste exercício são long length(), que
retorna o tamanho do arquivo correspondente à instância que recebe esta mensagem, e File[]
listFiles(), que retorna todos os arquivos contidos no diretório em questão. Observe que o
primeiro destes métodos só faz sentido quando aplicado a uma instância de File que faz
referência a um arquivo, enquanto o segundo método só se aplica a uma instância de File que faz
referência a um diretório.
27 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Decorator (estrutura)
O padrão Decorator adiciona uma camada de funcionalidade a uma classe sem o emprego de herança.
A classe resultante decora a classe original com alguma funcionalidade não disponível anteriormente.
Observe a classe Pessoa fornecida abaixo.
package decorator;
É provável que o estado de uma instância desta classe tenha que ser enviado para um arquivo texto,
não no formato retornado pelo método toString(), mas empregando valores separados por vírgulas,
ou seja, o formato CSV (Comma Separated Value). Em sentido contrário, você pode desejar inclusive
que instâncias de Pessoa possam ser criadas através de arquivos neste formado.
Para contemplar estas novas funcionalidades várias opções estão disponíveis. Uma delas é criar a
classe PessoaCSV, que herda da classe Pessoa e implementa métodos como toCSV() e
fromCSV(String). Além da minha falta de habilidade na identificação de um nome mais aprazível,
talvez toda a aplicação tenha que ser recompilada após as mudanças correspondentes serem aplicadas.
Da perspectiva do controle de qualidade esta opção pode ser inaceitável.
28 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
O cenário acima ainda pode ser tratado pelo emprego do padrão Decorator. As funcionalidades que
foram acrescentadas são mantidas em classes que não herdam da classe Pessoa. Podemos continuar
com a classe PessoaCSV, que agora não herda de Pessoa, mas é um decorador de Pessoa. Esta
abordagem é ilustrada abaixo.
A implementação deste modelo é fornecida abaixo. Convém ressaltar que, nesta abordagem, métodos
que implementam necessidades de software (tecnológicas) foram isolados daqueles que oferecem
semântica para o domínio em questão.
package decorator;
import java.util.StringTokenizer;
public PessoaDecorator(Pessoa p) {
this.p = p;
}
Observe que esta classe possui um único construtor que recebe como argumento uma instância de
Pessoa. O método toCSV() é trivial, simplesmente obtém os elementos que compõem o estado do
objeto Pessoa e os agrupa em uma seqüência de caracteres no formato CSV. O método
fromCSV(String) faz o oposto. Da seqüência de caracteres fornecida são obtidos os elementos que
compõem o estado do objeto Pessoa e, posteriormente, um a um são utilizados para atualizar o
estado deste objeto. O código abaixo permite testar esta funcionalidade.
package decorator;
import junit.framework.TestCase;
29 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Observe que a classe PessoaDecorator terá que ser alterada. A mudança é simples, pois esta classe
já implementa a interface CSV, falta apenas explicitar isto na declaração da classe conforme abaixo.
O diagrama correspondente é fornecido abaixo.
A classe CSVFileDecorator tem um construtor com dois argumentos: o nome de um arquivo e um
objeto. Será a partir do arquivo que uma linha CSV será obtida e para o qual uma linha CSV será
escrita. O objeto implementa a interface CSV. Qualquer objeto de uma classe que implementa esta
interface poderá ser fornecido. O conteúdo da linha CSV lida será depositado no estado do objeto
fornecido ou, conforme a requisição, será deste objeto o estado a ser convertido em uma linha CSV e
armazenada no arquivo fornecido. Esta classe é fornecida abaixo.
package decorator;
import java.io.File;
30 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Scanner;
Um simples teste é fornecido abaixo.
package decorator;
import junit.framework.TestCase;
31 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
decorator.fromFile();
A biblioteca Java também faz uso extensivo do padrão Decorator. As classe FileReader e
FileWriter, por exemplo, em geral são empregadas decoradas, respectivamente, pelas classes
BufferedReader e BufferedWriter. Todas estas classes pertencem ao pacote java.io. Observe que
BufferedReader oferece forma de leitura por meio do emprego de um buffer, o que não é fornecido
pela classe FileReader. A funcionalidade é similar, embora o ganho em desempenho possa ser
significativo. O mesmo ocorre com a classe BufferedWriter, que não escreve imediatamente após
cada requisição (conforme ocorre com a classe FileWriter), mas em um buffer antes que a escrita de
todo o buffer seja realizada. Apenas por precaução, é desnecessário afirmar que o Decorator não
necessariamente está associado a desempenho, conforme exemplo anterior.
No exemplo abaixo, o método main explicitamente faz uso do padrão Decorator ao criar a instância de
BuffereWriter, que decora a classe FileWriter.
package decorator;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.Writer;
interface Operacao {
void processa(int[] arrayInt) throws Exception;
}
32 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Exercícios
Uma aplicação que faça uso da tecnologia de Servlets cria, em geral, uma classe que trata requisições
oriundas de uma determinada página. Em um extremo podemos ter uma classe (um Servlet) para cada
página existente no sistema. Esta pode ser uma opção onerosa. A análise da interface
javax.servlet.ServeletRequest revela que um java.util.Map é empregado para encapsular
uma requisição. Para evitarmos um considerável número de Servlets, uma alternativa é empregar um
único Servlet que recebe requisições e as encaminha para classes que não necessariamente são
Servlets, mas que oferecem os serviços requisitados. Uma abordagem “similar” é realizada por
serviços web, onde as requisições são empacotadas em documentos XML.
Este exercício sugere que o padrão Decorator “decore” um java.util.Map onde as restrições
fornecidas abaixo devem ser observadas. Convém ressaltar que uma simples classe que realize os itens
(veja abaixo) talvez seja mais adequado que o emprego de um Decorator. Contudo, esta é apenas uma
simplificação de propósito didático. Conforme o exemplo empregando Servlets, em um cenário real
estaremos lidando com um Servlet e a adaptação da implementação do exemplo abaixo seria simples.
1. A classe ComandoMapDecorator tem construtor onde java.util.Map é o único argumento.
2. No objeto recebido como argumento do construtor todas as chaves são seqüências de caracteres,
assim como os valores correspondentes armazenados.
3. Uma única entrada deve ter como chave “interface” e como valor o nome completo de uma
interface, por exemplo, “br.ufg.inf.projeto.Servicos”.
4. Uma interface define várias operações. Para cada operação teremos pelo menos duas entradas.
Uma com a chave “operação” e o valor correspondente ao identificador da operação. A outra
entrada indica a quantidade de argumentos da operação. Neste caso, a chave é formada pelo nome
da operação concatenada com a seqüência “.args”. O valor correspondente é “0” para uma
operação que não possui nenhum argumento, “1” para operação com um argumento e assim
sucessivamente. Por exemlo, para a interface Servicos contendo a operação getValor(), a
instância de java.util.Map correspondente deverá possuir três entradas. A saber: (“interface”,
“Servicos”) , (“operacao”, “getValor”) e (“getValor.args”, “0).
5. Quando uma operação possui argumentos é necessário conhecer a ordem e o tipo de cada um
deles. Cada argumento de uma operação é fornecido em uma entrada própria, onde a chave tem
um formato próprio. Este formato é dado pelo identificador da operação concatenada com “.”
seguida da ordem do argumento para esta operação. O primeiro argumento é de ordem 1, o
segundo 2 e assim sucessivamente. Por exemplo, para a operação “sqrt(double)”, teríamos uma
entrada correspondente cuja chave é “operação” e o valor “sqrt” e outra entrada de chave “sqrt.1” e
valor “double”. Esta última entrada indica que o primeiro argumento da operação “sqrt” é do tipo
double.
6. A classe que implementa o padrão Decorator possui a operação execute(Object). Quando uma
instância desta classe recebe esta mensagem, a operação descrita no Map é executada sobre o
objeto fornecido como argumento.
7. Para requisitar dinamicamente a execução de um método podese empregar o código abaixo.
Convém ressaltar que clazz é uma instância de Class que representa o objeto que receberá a
33 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Façade (estrutura)
Um contador ao preencher o imposto de renda é uma forma de Façade, onde toda a parafernália da
contabilidade é ocultada do contribuinte por este prestador de serviços. Para o contribuinte, quando se
contrata um contador, não é necessário, embora conveniente, o domínio de expressões de cálculo, leis
e outros elementos relevantes para o preenchimento da declaração. O contador é um exemplo do
emprego do padrão Façade para a contabilidade.
De forma análoga temos um garçon em um restaurante. Felizmente, um jantar a dois em um lugar
romântico não exige habilidades culinárias do casal. Há um Façade atento para um conjunto de
mesas, o garçon, que se encarregará de tornar o jantar tão simples quanto requisitar e aguardar um
pouco. Isto depende do local, claro!
Quando se tem um subsistema que oferece considerável quantidade de serviços e apenas um
subconjunto deles é desejado, podese encapsular o subsistema em uma interface mais simples. Da
perspectiva do cliente apenas a interface terá que ser dominada, em vez de todo o subsistema. Este
encapsulamento é realizado por um Façade.
O padrão Façade é implementado como um intermediário entre o cliente e um subsistema. Além de
facilitar o uso, conforme apresentado acima, este padrão também permite maior independência do
cliente em relação ao subsistema. Quando um concorrente oferecer uma versão mais eficiente, você
não terá espalhado por toda a sua aplicação a API do subsistema anterior, que foi encapsulado pelo
Façade. A figura abaixo ilustra o padrão Façade.
<<subsystem>>
Complexo
A
ComplexoFacade
instance
Cliente B
+ metodo1()
+ metodo2()
+ getInstance() Z
Por simplicidade o modelo acima emprega uma subsistema identificado por Complexo e composto por
três classes. Adicionalmente foi acrescentada a classe ComplexoFacade, que implementa o padrão
Façade. Observe que esta classe é parte do subsistema. Em conseqüência, a classe Cliente, que
abstratamente representa o restante do código que faz uso dos serviços oferecidos pelo subsistema não
precisa conhecer os detalhes deste subsistema. Isto reduz o acoplamento geral da aplicação. Esta
redução também favorece a substituição do subsistema por outro, pois a dependência do restante da
aplicação está localizada apenas na classe ComplexoFacade.
Embora a ilutração acima tenha empregado uma única classe como ponto de entrada a todo um
subsistema, em cenários reais podem ser necessárias várias classes. Por exemplo, a API JDBC pode
ser vista como uma aplicação do padrão Façade composta por dezenas de classes e interfaces. Sem
34 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
esta API, toda aplicação em Java teria que conhecer e fazer uso de detalhes específicos da interface
oferecida pelo SGBD empregado.
Proxy (estrutura)
Em português, o termo proxy sugere algo como representante, procurador. No nosso contexto,
felizmente, o termo é sugestivo. Quando se emprega o padrão Proxy, sabese que um objeto tornase
representante de outro.
Esta representação é desejada quando, por exemplo, o objeto principal, aquele representado, possui
processo de criação oneroso e o acesso a ele deve ser controlado, entre outros casos. O representante
ou objeto Proxy age como um intermediário, como um procurador do objeto principal. Só se tem
acesso ao objeto principal pelo seu Proxy, o que permite o controle de acesso ao objeto principal, por
exemplo.
O famoso OpenOffice (http://www.openoffice.org/) compreende um editor de textos que ao abrir um
documento com figuras, não as exibe imediatamente, sem impedir que o usuário interaja com o texto
até que todas as figuras tenham sido carregadas e exibidas, o que pode ser um processo demorado
demais para um usuário. Uma resposta imediata pode ser obtida através do emprego do padrão Proxy.
Cada objeto figura possui um objeto Proxy correspondente. Quando o documento é aberto são os
objetos Proxy que irão responder pelos objetos que estes representam. Um comportamento possível é
retornar um simples quadro com o nome da figura. Esta resposta rápida pode ser visualizada pelo
usuário imediatamente enquanto o objeto Proxy realiza a carga da figura. Ao término, a figura real
substitui o quadro com o nome fornecido temporariamente. Navegadores (browsers) também
apresentam comportamento parecido.
Observe que se o texto é extenso e contém várias figuras, pode ser que um objeto Proxy nem mesmo
inicie o processo de carga imediatamente, mas apenas quando o usuário se deter em uma página na
qual uma figura deve aparecer. Neste exemplo, o objeto Proxy foi empregado como intermediário
com o propósito de melhorar o desempenho da perspectiva do usuário.
A GoF ilustra outros empregos deste padrão. Um objeto Proxy pode representar localmente um objeto
remoto. Para os clientes, o objeto de interesse parece disponível localmente. O objeto Proxy tornase
responsável por requisitar remotamente os serviços requisitados localmente. Um objeto Proxy pode
representar um objeto cujo processo de criação, conforme comentado nos parágrafos anterior, é lento
e, portanto, a ser realizado sob demanda apenas quando necessário. Um objeto Proxy pode proteger o
objeto principal de tal forma que toda mensagem tem a origem validada antes de chegar ao destino.
Um objeto Proxy também pode implementar um forma mais sofisticada de referência, onde um objeto
é carregado do meio persistente apenas quando uma mensagem que exige a disponibilidade do objeto
referenciado for enviada. Isto permite, por exemplo, o tratamento de extensas coleções de objetos em
memória. O código abaixo ilustra este caso onde cada objeto Texto representa um arquivo texto.
Observe que o arquivo correspondente é carregado toda vez que o nome do arquivo é alterado.
import java.io.FileInputStream;
import java.util.Scanner;
35 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Ao contrário da versão acima, a classe TextoProxy só irá realizar a carga do arquivo se for
necessário, e no último momento, o que é conhecido por lazy initialization. Este último instante é
detectado através do único método da classe que exige a carga do arquivo, o método toString().
Esta classe é derivada da classe Texto, ou seja, da perspectiva do cliente da classe Texto, tanto uma
instância desta quanto da classe TextoProxy pode ser fornecida, sem a necessidade do conhecimento
do cliente. O método setNome(String) também foi alterado.
Um teste de unidade é fornecido abaixo. Um objeto Texto e outro TextoProxy são criados. O
primeiro carrega o arquivo correspondente toda vez que o método setNome(String) é chamado. O
segundo só irá carregar o arquivo se for preciso. Neste exemplo, a última linha do código abaixo exige
a carga do arquivo, pois este deve ser impresso.
import junit.framework.TestCase;
36 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Interpreter (estrutura)
O padrão Interpreter é empregado para representar sentenças em uma determinada linguagem e para
interpretar sentenças nesta linguagem. O padrão não tem o propósito de substituir os sofisticados
mecanismos empregados na construção de compiladores, mas pode ser empregado efetivamente para
linguagens mais simples.
Uma linguagem “simples”, por exemplo, pode ser definida em um sistema de informação para a
geração de um arquivo de saída, para definir a ordem de saída das colunas de um relatório. Pode ser
necessário interagir com sistemas através de um formato que continuamente muda e, neste caso, um
interpretador pode ser definido para facilitar a geração de um leitor de arquivos neste formato volátil.
Expressões matemáticas simples também podem ser oferecidas aos usuários de um sistema, de forma
que este possa, por exemplo, definir qual a expressão de avaliação da média de um aluno.
Nos casos acima e em muitos outros, o interesse pelo padrão Interpreter está no fato de permitir que
especificações em um linguagem possam ser executadas sem a necessidade de se alterar, compilar e
posteriormente executar código. Um interpretador permite que sentenças na linguagem subjacente
possam ser produzidas em tempo de execução de um programa. Se for simples, então até um usuário
final pode fornecer sentenças que são prontamente executadas.
Se você está realmente interessado em construir a sua própria linguagem, então recomendo a leitura de
Building Parsers with Java, Steven John Metsker, AddisonWesley, 2001.
Template (comportamento)
O padrão Strategy (53) permite encapsular um algoritmo em uma classe. Este padrão permite que a
estrutura de um algoritmo seja definida por meio de métodos abstratos a serem implementados por
subclasses. Neste caso, cada subclasse implementa o algoritmo. Ao contrário do padrão Strategy, são
necessárias pelo menos duas classes por algoritmo: uma que define a estrutura invariável e outra que
fornece os métodos empregados por esta estrutura.
A classe abstrata Autenticacao fornecida abaixo, por exemplo, faz uso do padrão Template. Esta
classe abstrata encapsula o fluxo de autenticação de um usuário. Este encapsulamento é realizado pelo
método validaIdentidade(). Quando executado, este método primeiro irá obter a identidade a ser
avaliada. Isto é abstratamente realizado por meio do método obtemIdentidade(). Em seguida é
executado o processo de validação da identidade obtida. Novamente, não é especificado como esta
operação será realizada, o que é uma responsabilidade de uma implementação. Apenas é oferecida a
oportunidade de validar a identidade obtida pela ação anterior. Conforme o resultado desta validação,
uma mensagem de sucesso ou erro é informada. A mensagem pode ser um simples texto exibido na
saída padrão, o acréscimo de um registro em arquivo de log ou nenhuma operação sequer. Isto porque
tais detalhes não são fornecidos na classe abstrata, mas são de responsabilidade da classe que estende
esta classe abstrata.
37 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Observe que a classe Autenticacao é abstrata. Apenas uma subclasse concreta, que implemente os
métodos abstratos poderá dar origem a instâncias e, em particular, implementar o algoritmo que valida
a identidade de um usuário. O algoritmo neste caso é trivial, o primeiro método,
obtemIdentidade(), deverá retornar uma instância de Identidade, que corresponde ao nome do
usuário e a sua senha. A classe AutenticacaoGUI provavelmente empregará uma caixa de diálogo
para obter estas informações, enquanto a classe AutenticacaoConsole irá empregar a entrada e
saída padrãos. Ambas as implementações estarão disponíveis no método obtemIdentidade(). De
forma análoga cada um dos métodos abstratos deverão ser implementados.
Convém ressaltar que a classe concreta não herda apenas um conjunto de operações abstratas, como é
típico com o padrão Strategy. Neste caso, uma classe derivada de Autenticacao também herda um
algoritmo, cuja estrutura, a implementação empregando métodos abstratos, foi fornecida e não pode
ser alterada, observe o emprego do modificador final. Qualquer classe derivada deve apenas
preencher os espaços vazios, implementar os métodos abstratos.
Embora este exemplo seja simples, é fácil constatar o emprego do padrão Template. Desenvolvedores
web fazem uso extensivo da classe javax.servlet.http.HttpServlet, uma classe abstrata que
contém o método service(ServletRequest req. ServletResponse resp), entre vários outros.
Raramente este método é sobrescrito. Classes derivadas de HttpServlet reutilizam a elaborada
implementação deste método, que faz uso de métodos cuja implementação deve ser fornecida nas
classes derivadas, exatamente como o padrão Template. Ou seja, apenas a estrutura do algoritmo
implementado neste método é reutilizada. Os detalhes são fornecidos por classes derivadas.
Exercícios
1. Faça o modelo UML correspondente à solução empregada pela biblioteca Java para implementar o
método sort(Object[] a) fornecido pela classe java.util.Arrays. Explique como o padrão
Template está sendo empregado.
38 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Assuma que estas implementações devem ser executadas nesta ordem, caso a anterior não consiga
oferecer o serviço. Este é um caso natural para o emprego do padrão Chain of Responsibility.
Naturalmente, se a implementação que faz uso de FTP obter sucesso, nenhuma das outras estratégias
implementadas e disponíveis na cadeia será executada, pois o serviço já foi prestado. Caso contrário, a
próxima estratégia é executada. Também é possível que o emprego de email, por questões de
segurança, seja desabilitado por algum período e, neste caso, em tempo de execução, esta estratégia
pode ser removida da cadeia de implementações, assim como uma nova proposta pode ser inserida.
Como detectar um cenário onde o padrão Chain of Responsibility pode trazer benefícios? Algumas
aplicações, confesso que também já fiz assim, implementam esta seqüência de opções em um único
método. Inicialmente o método experimenta a solução preferida, se esta falha, então um teste conduz à
execução da próxima implementação preferida e assim por diante. O código deste método é difícil de
ser alterado. Coexistem, em um mesmo método, várias estratégias de solução, o que torna a coesão
baixa. Também não permite a inserção e remoção dinâmicas de estratégias pois, neste caso, código
deverá ser editado, compilado. A aplicação deverá ser interrompida para que a nova versão assuma o
lugar da anterior. Da perspectiva de teste temos um cenário ainda mais indesejável, pois não é
possível testar isoladamente a nova proposta de implementação. O padrão Chain of Responsibility não
possui nenhum destes inconvenientes.
Exercícios
Sistemas de informação exigem, em geral, vários valores necessários para que possam ser executados
de forma satisfatória. Faça uso do padrão Chain of Responsability para implementar uma aplicação
que oferece a flexibilidade exibida. O programa faz uso de informações de configuração obtida de
variáveis de ambiente. Uma variável de ambiente é obtida através de String getVar(String)
(método static) da classe Configuracao. Quando uma variável não é encontrada, a aplicação
procura por uma propriedade, que pode ser fornecida via linha de comandos, no momento da
execução do programa ou durante a leitura de arquivo de propriedades. Caso esta opção falhe, então
um arquivo de nome predefinido é consultado no diretório corrente. Caso esta última opção também
falhe, então um arquivo é obtido via Internet (servidor predefinido). Naturalmente, esta opção pode
resultar em falha e, neste caso, um portal alternativo, outro servidor, é procurado. Se e somente se as
opções anteriores falharem, então um valor predefinido é empregado. Para implementar este cenário
será útil o acesso aos serviços comentados abaixo:
1. Obtémse o valor de uma variável de ambiente através de System.getenv(String). Se a variável
não estiver definida, então o valor null é retornado.
2. Uma propriedade pode ser obtida com System.getProperty(String). Novamente, retorna null
caso se não existir propriedade com esta chave. Esta chamada pode gerar exceção, particularmente
quando não houver permissão suficiente para investigar estas propriedades. Uma propriedade pode
ser definida no momento da chamada da máquina virtual Java, conforme ilustrado abaixo
39 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Command (comportamento)
O padrão Command é empregado para encapsular uma requisição em um objeto. Em Java,
argumentos de métodos não incluem funções como em C/CC++. Se desejarmos que um método
receba uma função cuja execução será requisitada pelo método, então esta função terá que ser
implementada em uma classe cuja instância será fornecida ao método. Esta instância oferece uma
forma de representar uma requisição.
Um cliente pode depender da execução de uma tarefa que só é fornecida em tempo de execução.
Desta forma, quem recebe não sabe sequer quem irá oferecer o serviço e, se observarmos atentamente,
aquele responsável pela execução, conforme a classe acima, não sabe quem requisitou. Este é um dos
empregos do padrão Command.
Este padrão também permite desacoplar o sincronismo entre uma requisição e a execução imediata da
requisição. Todas as instâncias de classes que implementam a interface Command podem ser guardadas
em uma lista para posterior execução, talvez quando mais recursos estiverem disponíveis. Este pode
ser, inclusive, um mecanismo utilizado para a distribuição de carga entre processadores.
Estes empregos também sugerem um outro: reversão dos efeitos de uma operação (undo). Uma
interface como aquela abaixo seja empregada para encapsular o conjunto de requisições de interesse.
Para ilustrar, suponha que desejamos encapsular a requisição que altera o endereço de uma pessoa,
representada por uma instância da classe Pessoa, fornecida abaixo. Por simplicidade, apenas o
atributo endereco e métodos correspondentes foram fornecidos.
public class Pessoa {
private String endereco = null;
Um cliente que faz uso desta classe deseja representar a requisição de alteração de endereço. Conforme
visto anteriormente, o objetivo é permitir que esta requisição seja enfileirada efacilitar a operação que
desfaz a alteração de endereço, por exemplo. Esta requisição é implementada na classe abaixo.
40 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
A implementação fornecida acima exige a instância de Pessoa sobre a qual a operação será executada
e o valor do novo endereço a ser estabelecido para esta instância. Isto permite, por exemplo, que a
requisição possa ser enfileirada para execução posterior. Também é armazenado o endereço
imediatamente antes de uma alteração. É esta informação que permite recuperar (undo) o estado da
instância caso seja necessário desfazer a requisição. O programa abaixo ilustra o emprego desta
classe.
import junit.framework.TestCase;
cmd.faz();
assertEquals(“Rua Vila Nova, 150”, p.getEndereco());
cmd.desfaz();
assertEquals(“Rua das Vitórias, 143”, p.getEndereco());
}
}
É muito fácil confundir o padrão Command com o padrão Strategy. O padrão Command, contudo,
enfatiza o isolamento entre quem requisita e quem oferece um serviço. Serviço este que será oferecido
por um objeto, talvez empregando o padrão Strategy. Ao realizar este isolamento, vários benefícios
podem ser obtidos conforme visto acima. O emprego do padrão Strategy, por outro lado, não assegura
tais benefícios. O padrão Strategy, por exemplo, não visa facilitar a reversão de uma operação (undo).
O padrão Composite pode ser empregado para agrupar estratégias via o padrão Strategy, contudo, não
teria o efeito de uma macro quando o que é agrupado são objetos que implementam a interface
Command.
O padrão Strategy isola um algoritmo em uma classe. O foco é o algoritmo. O padrão Command
encapsula uma requisição propriamente dita, que será executada por um algoritmo. Estas requisições
podem ser agrupadas em uma fila, por exemplo. O foco é a requisição. Ambos fazem uso de uma
interface. No padrão Strategy a implementação da interface é um algoritmo. No padrão Command a
implementação da interface encapsula uma requisição. Da perspectiva de implementação, contudo, a
diferença praticamente não existe, embora visem propósitos distintos e empregos correspondentes
também distintos.
Exercícios
Faça uso do padrão Command de forma que as seguintes restrições são satisfeitas.
1. A requisição correspondente deverá ser executada quando a operação void faca() for
requisitada.
41 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
2. O resultado de uma requisição deverá ser desfeito se a operação void desfaca() for executada.
Convém ressaltar que nem todas operações são reversíveis.
3. Crie pelo menos duas classes que implementam a interface Command, cujas operações
correspondentes foram definidas no item acima.
4. Crie a classe ProcessadorCommand que executa uma fila de requisições, desde a requisição
corrente até a última. Esta classe também permite operações do tipo void faz(), onde a
requisição corrente é executada, void refaz() onde a requisição anteriormente executada é
reexecutada e void desfaz() onde a última requisição executada é desfeita.
5. A classe ProcessadorCommand também deverá ser passível de ser serializada e, em conseqüência,
uma determinada fila pode ser reexecutada, assim como o recurso de macros em editores de texto.
O método boolean salvar(String nomeArquivo) deverá ser empregado para armazenar o
conteúdo de uma fila de instâncias de Command em um arquivo e, para obter tais instâncias de uma
arquivo, use boolean carrega(String nomeArquivo).
Iterator (comportamento)
O padrão Iterator permite iterar por elementos de uma coleção de dados sem que se tenha
conhecimento dos detalhes de implementação destes dados. É comum o emprego deste padrão em
bibliotecas que implementam estruturas de dados. Java possui a interface Iterator<E> e a interface
Enumeration<E> ambas disponíveis no pacote java.util. Java também possui a interface
java.lang.Iterable<T>, que contém uma única operação (Iterator<T> iterator()).
O pequeno programa abaixo ilustra o emprego do padrão Iterator. De uma lista é obtido o objeto que
implementa a interface Iterator empregado para percorrer a lista.
package iterator;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
Iterator i = lista.iterator();
while (i.hasNext())
System.out.println(i.next());
O exemplo acima faz uso dos recursos disponibilizados pela biblioteca Java. O padrão Iterator,
contudo, não está restrito a tais casos. Ao construir uma estrutura de dados é comum permitir que
clientes desta estrutura possam iterar pelos elementos, conforme o exemplo abaixo.
42 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
package iterator;
import java.util.Iterator;
import java.util.NoSuchElementException;
Exercícios
1. Escreva uma aplicação que recebe, via linha de comandos, quando iniciada, uma seqüência de
caracteres. A saída da aplicação deverá ser a impressão de todas as letras contidas na seqüência
seguida da quantidade de vezes que a letra aparece na seqüência. A impressão deverá ser em
ordem decrescente da quantidade de aparições. Se mais de uma letra aparecer uma mesma
quantidade de vezes, então tais letras deverão aparecer em ordem alfabética. Assim, para a
seqüência, “carro”, teremos a letra r com duas ocorrências seguida, posteriormente, por três outras
linhas. A primeira linha contendo a letra a, a segunda contendo a letra c e a terceira contendo a
letra o. Este exercícios deverá, necessariamente, fazer uso do padrão Iterator. Não é necessário
implementálo, mas empregálo.
2. Implemente o trecho de projeto abaixo.
43 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Mediator (comportamento)
Software orientado a objeto envolve uma coleção de objetos que trocam mensagens entre eles.
Conforme a GoF destaca, em um caso extremo, todo objeto deve conhecer todos os demais. Isto
aumenta o acoplamento, que conduz a uma significativa dificuldade de manutenção. No caso extremo
citado, qualquer alteração deveria ser seguida de uma análise em todos os demais objetos do software,
o que seria impraticável para sistemas maiores.
O padrão Command encapsula uma requisição, ou o envio de uma mensagem de um determinado
objeto para outro sem que estes interlocutores conheçam um ao outro, o que reduz o acoplamento
entre estes. Contudo, o emprego deste padrão tem o propósito de oferecer algumas facilidades como
macros e undo, em vez de reduzir este acoplamento. Para reduzir o acoplamento o padrão Mediator é
uma proposta.
O padrão Mediator implementa o papel de intermediário à semelhança dos padrões Adapter e
Command, por exemplo. Ao contrário destes, contudo, o papel de intermediário tem como propósito
manter os vários interlocutores, e as complexas interações entre eles, sem que estes conheçam uns aos
outros. Em conseqüência, a dependência entre eles é reduzida e a possibilidade de reutilização das
classes correspondentes é maior.
O foco do padrão Mediator são as complexas interações entre objetos, encapsuladas em um objeto da
classe Mediator. Dado um conjunto de objetos onde a troca de mensagens entre eles é complexa, o
que pode ser comprovado pelo fato de que praticamente todo objeto deste conjunto conhece e depende
da existência dos demais, a idéia é concentrar a troca de mensagens na classe Mediator. Dessa forma,
a dependência entre os os objetos deste conjunto é substituída pela dependência entre estes objetos e o
objeto Mediator, que atuará como intermediário, sem que os seus interlocutores dependam
diretamente uns dos outros.
Exercícios
Faça uso do padrão Mediator para reduzir o acomplamento entre as classes que possuem o prefixo C.
1. Uma instância de CCasa, ao receber a mensagem acende(), envia a mensagem acendeLuz()
para as instâncias de CQuarto e CCozinha associadas. Cada uma destas, por sua vez, envia a
mensagem acende() para a instância de CLuz correspondente.
2. A instância CPortao, ao receber a mensagem abre(), envia a mensagem acendeLuz() para a
instância de CGaragem correspondente. Esta, por sua vez, envia a mensagem acende() para a
instância de CLuz pertinente.
3. A sua solução deverá fazer com que as seis dependências diretas, descritas acima, sejam
organizadas de tal forma que cada uma das classes possa ser empregada isoladamente das demais.
44 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Memento (comportamento)
A implementação da operação conhecida por undo pode se beneficiar do padrão Command. Para esta
finalidade é necessário guardar o estado de um objeto que posteriormente será restaurado. De fato,
para cada mudança no estado será necessário guardar uma cópia do estado anterior, antes da
efetivação da mudança, para que uma possível reversão seja viabilizada. O padrão Memento é uma
proposta para guardar o estado de um objeto sem violar o encapsulamento deste objeto.
Em Java, a implementação deste padrão pode ser realizada através da serialização do objeto. Ou seja,
em determinado instante, recuperar o estado anterior é tão simples quanto recuperar o objeto
serializado quando aquele estado foi obtido.
A GoF apresenta uma outra proposta de implementação onde pelo menos três classes distintas estão
envolvidas. Uma delas é a classe Memento, responsável por armazenar o estado do objeto
Originator cuja operação de undo deverá recuperar. Ou seja, Originator é a classe cujo estado
desejamos restaurar. Durante a execução de uma aplicação poderão existir várias instâncias de
Memento para um dado Originator. Estas instâncias de Memento são gerenciadas pelo objeto
Caretaker.
Exercícios
Implemente o padrão Memento para alguma classe que você considere em constante mudança de
estado e a operação de undo possa ser relevante para o domínio em questão. Ilustre a implementação
com a construção de um teste de unidade (faça uso do JUnit).
Flyweight (estrutura)
A graduação é uma experiência inesquecível para os alunos e, com certeza para os professores. Entre
um trabalho e outro é quase inevitável o surgimento de uma pérola.
Em um destes foi requisitado que os alunos criassem um programa que recebia como entrada um
número inteiro maior que zero. O programa deveria criar tantos retângulos quanto o número fornecido
como entrada. Nenhum retângulo deveria estar isolado, ou seja, pelo menos um dos lados deve possuir
fronteira com outro retângulo. O programa deveria arbitrariamente decidir o tamanho dos retângulos e
45 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
cada um deles deveria ser pintado com uma dentre um conjunto de quatro cores. A escolha deveria ser
simples: a cor escolhida para pintar o retângulo não poderia ser nenhuma das cores empregadas pelos
retângulos vizinhos.
Há um teorema que diz que todo e qualquer mapa planar pode ser pintado com apenas quatro cores
sem que vizinhos possuam a mesma cor. O objetivo do programa era mostrar uma evidência, apenas
uma evidência, de que o teorema está correto, ao contrário do que nossa intuição pode sugerir.
Uma solução comum foi criar tantas instâncias de retângulo quanto o número fornecido como entrada.
Para cada instância dessa era criada uma outra para a cor a ser empregada no momento de pintar o
retângulo e uma instância de Ponto para indicar a posição em que o retângulo deveria ser desenhado.
A instância ainda possuía duas variáveis para indicar os lados do retângulo. A classe se parecia com
aquela abaixo. Apenas o construtor e os atributos são fornecidos por simplicidade.
Resultado. Para números suficientemente grandes os alunos descobriram que a memória do
computador é finita (“out of memory”). Não era claro, para muitos deles, que uma outra estratégia
diferente poderia ser empregada. Tal estratégia é o padrão Flyweight.
Observe que temos apenas quatro cores, qualquer que seja a quantidade de retângulos. São
necessárias, portanto, apenas quatro instâncias de Cor e não tantas quantas forem o número de
retângulos. Em conseqüência, a classe acima pode ser reescrita sem que uma nova instância de Cor
seja criada para cada retângulo, mas que uma das quatro instâncias disponíveis seja reutilizada. Esta
estratégia emprega o padrão Flyweigth aplicado à classe Cor.
public class Retangulo {
private Cor cor;
private Ponto ponto;
private float base;
private float altura;
Observe que agora não mais será criada uma nova instância de Cor e uma nova instância de Ponto, ao
contrário do exemplo anterior. Isto não impede, contudo, que tenhamos uma instância destas classes
para cada instância de Retangulo. De fato, teremos que ter uma nova instância de Ponto, mas não
necessariamente teremos que ter uma nova instância de Cor, conforme comentado anteriormente.
Para evitar a criação de uma nova instância de Cor será necessário a criação de um Factory. Toda
reuisição a este Factory irá verificar se uma instância de Cor já existe. Caso afirmativo, então esta
instância será retornada. Caso contrário, a instância é criada e guardada para possível referência
posterior. Primeiro observe a classe Cor, fornecida abaixo, onde apenas o construtor, por simplicidade
e os atributos são exibidos.
46 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
O emprego do padrão Factory para a classe acima é relativamente simples. Assumindo, como neste
caso, um número reduzido de possibilidades, um Set pode ser empregado. Primeiro tentase recuperar
o objeto, caso não esteja disponível, então este é inserido. Observe que, neste caso, será necessário
implementar os métodos equals e hashcode para a classe Cor.
Outro exemplo. Em um sistema onde a classe Icone representa um ícone a ser pintado em
determinada posição da tela e sabemos que o número de ícones disponíveis é relativamente pequeno,
independente do número de vezes em que instâncias desta classe deverão ser criadas, podemos
empregar o padrão Flyweigth.
Neste caso, a classe Icone possui dados extrínsecos: a posição em que o ícone deverá ser pintado.
Esta informação, portanto, não pode ser compartilhada e deverá ser armazenada pelo cliente pois, com
certeza, toda vez que um ícone for desenhado será necessário fornecer esta informação que não será
por instâncias desta classe. Dado que o número de ícones é relativamente pequeno, podemos criar
uma instância para cada um deles e, a partir deste ponto, compartilhar estas instâncias criadas tantas
quantas forem as vezes em isto se fizer necessário. Em uma abordagem que não emprega este padrão,
toda vez que um ícone fosse empregado teríamos que criar uma nova instância.
Exercícios
Implemente o padrão Flyweight para o exemplo da classe Cor, comentado acima.
Observer (comportamento)
O padrão Observer tem pelo menos duas outras denominações: Publish/Subscribe e DEM (Delegation
Event Model). O objetivo é estabelecer um relacionamento entre objetos de tal forma que a ocorrência
de um evento em um objeto, por exemplo a alteração do estado deste, possa repercutir em objetos
interessados neste evento. Neste relacionamento há objetos que geram eventos (publishers) e aqueles
que estão atentos à ocorrência destes eventos (subscribers).
Em uma aplicação onde a interface apresenta várias visões de uma mesma informação, por exemplo,
cada objeto responsável por uma visão registra (subscribe) o seu interesse com o objeto que contém a
informação que pode ser alterada (publisher). Uma vez realizado o registro, toda vez que alguma
operação altera o valor da informação, todos os objetos interessados são avisados, o que oferece
oportunidade para que estes possam refletir o novo estado da informação.
Neste caso, o padrão Observer implementa o relacionamento entre um objeto que detém a informação
e aqueles interessados no estado desta informação, sem que o primeiro esteja acoplado aos últimos.
Para ilustrar, imagine uma aplicação que continuamente exibe o valor da temperatura corrente,
registrada em uma instância da classe Temperatura. Toda vez que o valor é alterado, possivelmente
através da leitura constante de um sensor, a alteração deve ser exibida para o usuário.
A exibição pode ser realizada pela classe ExibeTemperatura. Podem existir outros objetos, por
exemplo, uma instância da classe ExibeAlertaTemperatura, responsável por emitir aviso sonoro e
visual que chame a atenção do operador para um valor crítido da temperatura. Naturalmente, todas
estas questões são relevantes mas não deveriam ser implementadas na classe Temperatura, pois esta
47 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
classe é parte da semântica da aplicação, enquanto as demais fazem parte da apresentação, têm
propósitos distintos e, portanto, as tarefas que elas realizam devem estar isoladas.
Um outro objeto pode ser responsável pela desativação automática da máquina cuja temperatura está
elevada. Neste caso, tal objeto não é da camada de apresentação, mas um objeto da camada de
negócio interessado em evento em outro objeto desta mesma camada. O padrão Observer oferece uma
solução elegante para o relacionamento entre estes objetos sem que a instância de Temperatura esteja
acoplada a qualquer um dos demais objetos deste exemplo.
A idéia do padrão Observer não é complexa. Todo objeto que se interessa pela ocorrência de
determinado evento é um observador interessado em algo observável. A biblioteca Java possui uma
interface e uma classe para representar estes papéis: java.util.Observer e
java.util.Observable. A biblioteca Java apenas facilita a implementação deste padrão através
deste tipos, contudo, não é necessário empregálos para se ter acesso aos benefícios deste padrão e, de
fato, há quem sugere que estes sejam evitados. Esta opção é empregada aqui por simplicidade.
O valor da temperatura pode ser exibido graficamente, através de um termômetro com a marca da
temperatura corrente, assim como através de uma pequena luz, vermelha quando a temperatura
ultrapassa determinado valor e verde em caso contrário. Outras visões do termômetro podem ser
empregadas. Por simplicidade, contudo, empregaremos outras duas, ambas textuais. A primeira delas
é fornecida abaixo. Uma instância desta classe simplesmente exibe o valor da temperatura corrente na
saída padrão.Um comportamento trivial para uma classe da camada de apresentação, mas didática.
Conforme ilustrado, a interface Observer exige a implementação da operação update.
public class ExibeTemperatura implements Observer {
public void update(Observable observable, Object obj) {
Temperatura t = (Temperatura) observable;
System.out.println(“Temperatura corrente: “ + t.getTemperatura());
}
}
A outra classe é fornecida abaixo. A implementação, neste caso, imprime uma mensagem conforme o
valor da temperatura.
public class ExibeAlertaTemperatura implements Evento {
public void mudanca(Temperatura t) {
Temperatura t = (Temperatura) observable;
if (!t.isNormal()) {
System.out.println("[ALERTA] Temperatura elevada: " + t.getTemperatura());
}
}
}
As duas classes acima podem, conforme dito anteriormente, exibir tais informações de forma gráfica.
O texto é uma simplificação para ilustrar o padrão Observer. A classe ExibeAlertaTemperatura faz
uso de método que requisita do objeto do domínio informação quanto à normalidade do valor da
temperatura. Afinal, não é a camada de apresentação que sabe quando uma temperatura está elevada
ou não.
Ainda nos resta uma aplicação de teste e a classe Temperatura. A classe Temperatura tem o método
setTemperatura(double) através do qual sensores podem indicar o valor da temperatura corrente.
Quando este método é chamado, conforme código abaixo, duas mensagens são envidas. Uma indica
que o valor foi alterado e a outra oferece a possibilidade para que cada Observer possa reagir em
relação à mudança. É notório que a classe Temperatura não conhece nenhuma classe que depende do
valor por ela gerido.
public class Temperatura extends Observable {
private double temperatura;
48 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Deve ficar claro que qualquer objeto que implemente Observer poderá registrar o seu interesse em
alterações no estado de uma instância de Temperatura. Ou seja, outras classes além daquelas
fornecidas podem ser criadas, sem que esta criação ou a alteração naquelas fornecidas afete a classe
Temperatura. Esta é a independência que tanto se falou nos parágrafos anteriores e permite a
independência entre a camada de apresentação e a camada de negócio. Ainda nos falta uma classe de
teste e outra que faça a simulação de um sensor de temperatura. A de teste é fornecida abaixo. Dois
observadores são associados a uma instância de Temperatura (objeto observado). Logo em seguida a
instância de Sensor é associada ao objeto criado de Temperatura. Por fim, indicamos que deverá ser
feita uma leitura do sensor a cada segundo.
public class TestaObserver {
public static void main(String[] args) {
ExibeTemperatura exige = new ExibeTemperatura();
ExibeAlertaTemperatura alerta = new ExibeAlertaTemperatura();
t.adicionaSubscriber(exibe);
t.adicionaSubscriber(alerta);
O código acima depende da classe Sensor. No nosso exemplo didático não é necessário empregar um
sensor real. Contudo, podemos simulálo conforme abaixo. Observe que a cada leitura que fazemos do
sensor, através do método run(), um novo valor para a temperatura é aleatoriamente estabelecido
para a instância de Temperatura.
public Sensor(Temperatura t) {
temp = t;
}
49 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
State (comportamento)
O padrão State é empregado para implementar diagramas de transição de estados. Em tais diagramas
existem estados e transições entre eles. Eventos provocam transições que, por sua vez, disparam a
execução de ações. Tanto as transições quanto as ações dependem do estado ativo no momento em
que o evento em questão é gerado. Está além do escopo deste texto apresentar detalhadamente um
diagrama de transição de estados e sua extensão utilizada pela UML (os Statecharts).
São muitos os cenários onde uma diagrama de transição de estados pode ser adequadamente
empregado para modelálo. Em um editor gráfico, por exemplo, se o usuário realiza um clique sobre
um objeto, então este objeto sob a posição do mouse é selecionado. Contudo, se o editor se encontrar
no modo ou estado de “Criação de figura”, onde a opção círculo encontrase selecionada, então um
círculo com centro na posição onde se encontra o mouse será desenhado, ao contrário de uma seleção
(estado anterior). O padrão State visa facilitar a implementação de cenários como este, ou seja, a
implementação de código que representa comportamentos distintos conforme o estado corrente. Em
tempo, observe que no exemplo acima uma mesma ação do usuário foi interpretada diferente,
conforme o estado no momento em que o evento foi gerado.
A implementação sugerida deste padrão faz uso de uma interface que deverá definir todos as reações
possíveis para todos os estados a serem contemplados. Seja Estado esta interface. Cada reação pode
ser definida na interface através de operações que representam os eventos possíveis. Para todo e
qualquer possível evento haverá uma operação correspondente nesta interface. Será a execução destas
operações que irão provocar mudanças do estado corrente. Cada estado possível do diagrama será
representado por uma classe que implementa a interface Estado e, naturalmente, possui uma
implementação para cada operação (evento). É cada implementação específica que identificará como é
que o estado em questão reage ao evento gerado.
Na figura abaixo a classe Porta representa toda e qualquer porta que pode estar em um de dois
estados: aberta ou fechada. Cada um destes estados é representado por uma classe que implementa a
interface Estado. Respectivamente: Aberto e Fechado. É uma instância de Porta que recebe eventos
(mensagens) como abrir ou fechar. Neste exemplo, uma porta pode receber estes dois únicos eventos,
conforme modelado abaixo.
A reação de uma instância de Porta a um evento (mensagem abrir() ou fechar()) é simples. Se o
estado corrente da porta é uma instância de Fechado, ao receber o evento fechar() não haverá o que
fazer (este comportamento estará implementado no método fechar() da classe Fechado). De forma
similar, se o estado corrente de uma instância de Porta for uma instância de Aberto e o evento gerado
é abrir(), então não há o que fazer (à semelhança do caso anterior, a implementação do método em
questão, abrir(), será nula). Contudo, uma porta aberta pode receber o evento fechar e, neste caso, há
50 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
uma reação prevista e uma mudança de estado, pois após este evento o estado da porta passa de aberto
para fechado. Antes de observar como implementar este comportamento, o diagrama abaixo ilustra o
comportamento descrito por meio de um diagrama de transição de estados, mais especificamente, um
Statechart (variante de tais diagramas empregada pela UML).
Dito de outra forma, antes de apresentar detalhes de implementação, um cenário onde se tem uma
porta aberta será representado por uma instância de Porta que referencia uma instância de classe que
implementa a interface Estado que, neste caso, será uma instância de Aberto. Eventos foram
modelados como mensagens, ou seja, esta porta aberta pode receber o evento fechar ou, conforme
modelado, a mensagem fechar() e, neste caso, o estado corrente passará a ser fechado. Ou seja, a
referência que Porta retém para Estado passará de uma referência para uma instância de Aberto
para uma instância de Fechado. Por fim, o estado “Aberto”, ilustrado acima, não apresenta uma
transição rotulada com o evento abrir, o que significa que este evento, se gerado neste estado, não terá
nenhum efeito. De forma análoga, o estado “Fechado” não possui nenhuma transição rotulada com o
evento “fechar”, o que torna a ocorrência deste evento quando este estado estiver ativo um fato
irrelevante, sem efeito.
No diagrama de transição de estados acima, o estado inicial é “Fechado”. No modelo este estado é
representado por uma instância da classe Fechado. Este estado pode ser alterado para “Aberto”
(representado por instância da classe Aberto) com a ocorrência do evento “abrir” (modelado como
método abrir() definido na interface Estado). Ao representar o cenário da uma porta que pode estar
aberta ou não e que pode ser aberta ou fechada, conforme feito acima, estáse empregando o padrão
State. Alguns detalhes de implementação são comentados a seguir. Em particular, como identificar o
estado corrente? Como ocorre a transição de um estado para outro?
Observe que a classe Porta possui a referência corrente para o estado corrente. Para identificar este
estado, existem várias possibilidades, desde empregar o operador instanceof de Java para identificar
qual o objeto que implementa a interface que está sendo referenciado ou, mais simplesmente, por
meio da mensagem getNome(), que deve ser implementada por toda implementação de Estado. Isto
apenas responda a primeira pergunta acima. A mais relevante é como se alterna de um estado corrente
para outro?
Toda implementação de Estado pode receber todo e qualquer evento que pode ser gerado para uma
porta. No caso da classe Fechado a implementação de fechar() é trivial, pois não há o que fazer
quando uma porta se encontra fechada e o evento “fechar” é gerado. Ou seja, a implementação é
fornecida abaixo.
public void fechar() {}
Por outro lado, se o evento gerado é “abrir”, então a porta deverá ser aberta ou, conforme modelado, o
estado corrente deve passar de uma instância de Fechado para uma instância de Aberto.
Naturalmente, este conhecimento deverá estar implementado no método abrir() da classe Fechado.
A implementação correspondente é ilustrada abaixo e facilita a compreensão.
public void abrir() {
setEstado(ABERTO);
}
Acima foi empregada a constante ABERTO para representar a instância correspondente ao estado
“Aberto”, ou seja, uma instância da classe Aberto. A implementação também foi trivial. Observe que
51 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
não há ação a ser executada quando se sai do estado “Fechado” nem ação correspondente quando se
percorre a transição e o estado “Aberto” é ativado. Ou seja, apenas a atualização do estado corrente
precisa ser efetuada. A implementação de setEstado(Estado) também é simples, conforme abaixo, ao
fazer uso de uma variável compartilhada por todas as instâncias que implementam Estado.
public void setEstado(Estado corrente) {
EstadoCorrente.corrente = corrente);
}
Há várias possibilidades de implementação não comentadas. O objetivo foi ilustrar e permitir a
compreensão do padrão State, que pode ser empregado em vários outros contextos. Em geral,
qualquer que seja o objeto cujo comportamento parece ser alterado conforme o estado em que se
encontra, é um candidato a ser implementado com apoio do padrão State.
Exercícios
1. Implemente o padrão State para o exemplo da classe Porta comentado acima.
2. Faça uso do padrão State para implementar o comportamento registrado no diagrama abaixo.
Este comportamento correspondente ao comportamento esperado para toda instância da
classe Processo. Crie a classe Processo e implemente o comportamento de tal forma que,
dada a ocorrência de um evento, possivelmente ocorre a transição para um dado estado,
juntamente com a execução das ações julgadas oportunas. Neste caso, apenas uma ação deve
ser executada como resultado da saída do estado “Em avaliação”. Implemente a ação “avisar
interessado” por meio de uma simples mensagem produzida na saída padrão. Um cenário real
poderia exigir o envio de correspondência eletrônica para o email do interessado, o que não é
exigido neste exercício. Por fim, observe que o estado “Avaliado” é uma composição dos
estados “Indeferido” e “Deferido”. São estes dois últimos que deverão ser tratados. Ou seja,
“Avaliado” é apenas uma abstração que não precisa ser tratada da perspectiva de
implementação. Observe que, transcorrido o prazo legal após avaliação de um processo, este
é conduzido ao estado “Fechado”, independente se o estado é “Indeferido” ou “Deferido”.
3. Implemente o exercício anterior empregando a classe abstrata Estado, em vez da interface
Estado.
4. Implemente o exercício 2 empregando o padrão State onde a interface Estado possui a
operação getContexto(Contexto) onde Contexto é interface implementada por todo
objeto cujo comportamento está sendo modelado por diagrama de transição de estados e cuja
52 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
implementação faz uso do padrão State. Neste caso, tal operação será empregada por uma
instância que é um Estado para alterar o estado corrente referenciado pelo objeto cujo
comportamento está sendo implementado por este padrão. Para isto, A interface Contexto
ainda deverá disponibilizar a operação setEstadoCorrente(Estado).
Strategy (comportamento)
Implementar algoritmos onde cada um deles pode ser substituído por outro, independente do cliente
que o utiliza, é uma tarefa para o padrão Strategy. Um algoritmo pode ser útil em determinado
contexto e inadequado em outro. Em cenários como este, a coexistência de algoritmos é desejável,
sem que a escolha de um deles, mais apropriado ao contexto em questão, provoque mudanças nos
clientes.
A ordenação, por exemplo, é apoiada por toda uma coleção de algoritmos. Alguns algoritmos
consomem mais memória que outros e, em um cenário onde este recurso é escasso, a melhor
alternativa e fazer uso de um algoritmo parcimonioso.
Em um ambiente comercial podem existir várias regras, que se alteram ao longo do tempo, com o
propósito de premiar o desempenho de vendedores. Em determinado momento, por exemplo, a maior
quantidade de vendas por mês no último ano é a regra. Um novo gerente provavelmente irá substituí
la por outra. Mais uma vez o padrão Strategy pode ser utilizado para proteger o restante do sistema do
algoritmo adotado.
Mais um cenário de emprego. Em sistemas acadêmicos, uma estratégia pode enfatizar a presença do
estudante em sala de aula no seu desempenho, outra pode fazer uso exclusivo dos resultados das
avaliações, enquanto outro, mais holístico, pode considerar vários fatores como o uso da biblioteca,
desempenho em atividades extracurriculares e outras. Talvez seja interessante a convivência pacífica
de todas estas estratégias, empregadas em momentos distintos, ou mesmo simultaneamente.
Quando este padrão não é empregado em cenários como aqueles ilustrados acima, é comum a
presença de estruturas de decisão complexas onde as condições estabelecem quando uma estratégia
deve ou não ser empregada. Em conseqüência, este padrão é empregado quando se deseja eliminar
uma estrutura de decisão que controla várias variantes.
Dinâmica
O padrão Strategy emprega três colaboradores: uma interface que encapsula os algoritmos; classes que
implementam a interface, ou seja, a implementação dos algoritmos e uma classe cliente, que faz uso
de um algoritmo. O cliente emprega uma instância de uma das classes que implementa a interface.
Este emprego é realizado sem que o cliente saiba exatamente quem está oferecendo o serviço.
Supondo a existência de um único algoritmo, seja este de ordenação de inteiros fornecidos em um
array, podemos definir a seguinte interface:
public interface SortStrategy {
void sort(int[] int);
}
Existem vários algoritmos de ordenação. Todo aquele que o cliente poderá fazer uso deverá ser
encapsulado em uma classe que implementa a interface SortStrategy. Um famoso método de
ordenação conhecido por Quicksort, por exemplo, pode ser implementado conforme abaixo. A
implementação do algoritmo foi omitida, mas pode ser encontrada facilmente na literatura.
public class QuickSort implements SortStrategy {
void sort(int[] ints) {
// Implementação do QuickSort
}
}
53 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Se houver interesse em outro algoritmo de ordenação, por exemplo a ordenação pelo método da bolha,
então o algoritmo correspondente deverá ser encapsulado em uma classe como aquela abaixo.
public class BubbleSort implements SortStrategy {
void sort(int[] ints) {
// Implementação do BubbleSort
}
}
Neste ponto, todos os algoritmos de interesse estão devidamente encapsulados em suas respectivas
classes. Cada uma destas implementa a interface SortStrategy.
O cliente, conforme ilustrado abaixo, faz uso do serviço de ordenação, ou seja, de um objeto de uma
classe que implementa SortStrategy. Este cliente, em particular, precisa do serviço de ordenação,
independente do algoritmo que a implementa. É fácil observar que a classe Cliente desconhece a
existência das classes QuickSort e BubbleSort. Ou seja, é independente destas implementações. O
serviço é usufruído através de uma referência para SortStrategy. Ao ser criada uma instância de
Cliente, uma instância de uma classe que implementa SortStrategy deverá ser fornecida.
O exemplo acima elucida os principais elementos que compõem o emprego do padrão Strategy.
Contudo, você terá que fornecer mais código para poder executálo. Aquele abaxo, ao contrário, está
completo, além de também servir como exemplo. Por simplicidade, todo o código é fornecido em um
único arquivo. Uma pequena explanação é fornecida logo após o código.
interface ImprimeArray {
void imprime(int[] a);
}
class Cliente {
private ImprimeArray srvImpressao;
private int[] ints = new int[] { 1, 2, 3, 4, 5 };
54 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
srvImpressao.imprime(ints);
}
}
c1.exibeEstado(); // Imprime 1 2 3 4 5
c2.exibeEstado(); // Imprime 5 4 3 2 1
}
}
Exercício
A seqüência de passos abaixo ilustra o emprego do padrão Strategy para eliminar, ou melhor,
substituir uma estrutura de decisão complexa. O cenário é uma empresa que precisa calcular o
desconto que oferece aos seus clientes em cada venda. O desconto é baseado no histórico de vendas
do cliente.
1. Dizse que dois objetos wrapper do tipo Short que representam o mesmo valor primitivo na
faixa de 128 e 127 são iguais, conforme o operador ==, se obtidos por meio do método
estático valueOf(short) ou valueOf(String). Contudo, se duas instâncias de Short
forem obtidas a partir do emprego do operador new, então o resultado são dois novos objetos.
O mesmo é dito para objetos do tipo Integer. Ou seja, a comparação empregando ==
retornará true para duas instâncias de Integer que representam o valor 127, por exemplo,
quando obtidas por meio de Integer.valueOf(127), enquanto esta mesma comparação irá
retornar o valor false caso o valor primitivo representado seja 128. Esta afirmação também é
válida para o tipo Byte. Escreva um programa em Java que teste a veracidade destas
afirmações para os tipos Byte, Short e Integer. O padrão Strategy deverá ser empregado.
2. Crie a interface Java Tarefa contendo uma única operação: int executa(). Crie a interface
Java Concorrente que estende a interface Tarefa. Contudo, nenhuma operação será
adicionada. Neste caso, qualquer implementação da interface Concorrente deverá executar a
tarefa implementada pelo método int executa() de forma concorrente. Crie a interface
Java Sequencial que também estende a interface Tarefa sem acrescentar operações. Uma
implementação desta interface, ao contrário de uma implementação da interface anterior,
deverá executar a tarefa correspondente de forma seqüêncial, ou seja, sem o emprego de
concorrência (threads). Escreva a classe Tempo cujo construtor recebe uma referência para
Tarefa como argumento. Esta classe deve implementar a interface Tarefa e, quando o
método executa() for chamado, deverá executar a tarefa implementada pelo objeto recebido
pelo construtor e computar o tempo gasto para a execução da tarefa correspondente. A classe
Tempo não possuirá outra dependência senão para a interface Tarefa. Crie um programa em
Java que use a classe Tempo para verificar que, conforme a documentação da API de Java
informa, a classe StringBuilder é “mais rápida” que a classe StringBuffer, embora
ambas ofereçam as mesmas funcionalidades. Crie um segundo programa em Java que faça
55 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
uso preferencial das interfaces fornecidas acima, se for o caso, para verificar que, de fato, a
classe StringBuilder não é segura se empregada por vários threads, ao contrário da classe
StringBuffer que, ao contrário da anterior, é dita thread safe.
3. Uma empresa que comercializa vários produtos possui um esquema de desconto conforme
algumas regras. Regras de desconto para compras: (a) se o cliente é “antigo” (mais de dois
anos) o desconto é de 2% + 0,12% por cada ano de clientela além de dois anos (qualquer que
seja o produto adquirido) até um desconto adicional máximo de 1%, o que perfaz um total de
no máximo 3%; (b) se o cliente é “antigo” e “freqüente” (comprou nos últimos três meses)
então o desconto é de 3% para os produtos A e B adquiridos em quantidade superior a 20
unidades; (c) se o cliente compra acima de 5% do estoque disponível, então o desconto é de
2% para o produto e de 1,5% para os demais produtos. O que se observa neste pequeno
número de regras é que as condições são relativamente simples, assim como as expressões de
cálculo correspondentes. Outros cenários, contudo, podem apresentar bem mais
complexidade. Neste exercício você deverá implementar tais regras com o padrão Strategy. A
interface DescontoStrategy deve ser criada. Esta interface inclui a operação double
desconto(). Uma implementação desta interface será criada para cada regra de desconto.
Por exemplo, para a primeira regra pode ser criada a classe ClienteAntigo. Naturalmente, a
implementação de desconto() irá retornar a soma de 2% e 0,12% para cada ano além de
dois anos de clientela até o limite de 1%, conforme a regra supracitada. Para este exercício
serão necessárias três implementações de DescontoStrategy. A classe NotaFiscal poderá
ter acesso à implementação a ser utilizada por meio do construtor e, neste caso, fica sob a
responsabilidade do cliente desta classe identificar qual a regra ou conjunto de regras a ser
empregado. Uma alternativa é empregar o padrão Factory para decidir qual a implementação
a ser empregada conforme a compra e o cliente. Em qualquer um destes casos, é necessário
conhecer todos os itens da nota para se obter o desconto final, pois várias regras podem ser
aplicáveis em cenários distintos, assim como informações sobre o cliente. Independente do
caso, a NotaFiscal simplesmente delegará para o conjunto de estratégias correspondente o
cálculo do desconto. Convém ressaltar que, nesta proposta, não é a instância de NotaFiscal
a responsável pela identificação do desconto. Esta responsabilidade foi transferida para um
conjunto de implementações de DescontoStrategy. Defina o projeto UML correspondente
à implementação requisitada deste padrão. Onde detalhes forem necessários mas não
fornecidos pelo exercício, complementeo da forma que considerar apropriado.
Visitor (comportamento)
Quando se deseja realizar uma operação em um conjunto de objetos que possuem interfaces distintas,
ou seja, as classes correspondentes são de hierarquias distintas, não é possível fazer uso de
polimorfismo.
Mesmo que polimorfismo seja uma opção, acrescentar métodos de naturezas distintas em uma mesma
classe reduz a coesão desta classe. Por exemplo, no exercício pertinente ao padrão Composite (26) foi
sugerida a criação da classe Elemento, ancestral tanto de Arquivo quanto de Diretorio. A classe
Elemento define a operação long tamanho(), que retorna a quantidade de bytes do arquivo ou
diretório correspondente. Se em um momento futuro desejarmos a geração de um relatório contendo
detalhes da permissão de acesso para cada arquivo e diretório, por exemplo, então estaremos reunindo
em uma só classe, a contagem do tamanho de um arquivo e questões de permissão. Estas operações
até que podem coexistir sem significativa perda de coesão, contudo, uma operação que abre cada
arquivo e identifica qual o formato deste, por exemplo, executável, PDF ou outro, já envolve tarefas
bem distintas das anteriores.
Se isto não é suficiente, então podese sugerir um infindável lista de tarefas e, seguindo este modelo,
todas estariam em uma única classe. O padrão Visitor é uma forma de implementar os serviços
56 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
necessários em classes distintas, isoladas daquelas que compõem a estrutura de objetos que desejamos
trabalhar.
Também convém ressaltar que às vezes é necessário realizar operações distintas que nem mesmo
foram previstas no momento de definição das classes. Nestes casos, o padrão Visitor também nos
oferece uma alternativa.
Para fazer uso deste padrão convém identificar duas classes distintas. Uma é a classe abstrata
Visitor. Classes concretas derivadas de Visitor são responsáveis por implementar as operações
descritas acima. Para cada operação deve ser definida uma classe concreta que herda de Visitor. A
operação é implementada no método visit(TipoClasse), onde TipoClasse é o nome da classe
cujas instâncias serão visitadas, ou seja, a operação está sendo definida para objetos do tipo
TipoClasse, conforme o código abaixo.
Caso a classe concreta implemente a operação para mais de um tipo de classe, então a única operação
fornecida acima deverá ser sobrecarregada. Neste caso, deverão existir tantas operações quantas forem
as classes dos objetos que podem ser visitados. Por exemplo, se a classe Computador e a classe Livro
possuem instâncias que deverão ser processadas por tais operações, então a classe acima deverá ser
reescrita como abaixo.
Dessa forma, toda classe concreta que herda daquela acima poderá realizar uma operação tanto para
instâncias de Computador quanto para instâncias de Livro. Para que isto seja possível é necessário
que as classes Computador e Livro implementem o método abaixo.
Dinâmica
Quando desejarmos que uma determinada operação seja realizada para instâncias da classe X, esta
operação deverá ser implementada em uma classe que herda de Visitor através do método
visit(X). Digamos que Operacao seja o nome desta classe. A classe X, conforme comentado
anteriormente, deverá implementar o método accept(Visitor).
Neste cenário, as instâncias para as quais a operação será realizada deverão receber a mensagem
accept(op) onde op é uma instância de Visitor. O cliente do padrão Visitor deverá, naturalmente,
conhecer tanto as instâncias quanto o objeto que implementa a operação.
Cada mensagem enviada por este cliente, conforme o código do método accept(Visitor),
fornecido acima, dará origem a uma outra, desta vez, da instância sobre a qual a operação será
realizada para o objeto que implementa a operação. O objeto que implementa a operação é fornecido
através do argumento, desta forma, o próprio objeto poderá requisitar ao objeto op a execução da
operação que este implementa.
Este modelo faz uso do que é conhecido por doubledispatching, pois o cliente poderia simplesmente
informar ao objeto que implementa a operação qual o objeto sobre o qual a operação deverá ser
realizada. Ao contrário, o cliente envia uma mensagem ao objeto e este, conforme o argumento
fornecido, envia a requisição da operação ao objeto que a implementa.
57 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
Exercícios
Implemente o padrão Visitor para uma estrutura de objetos onde arquivos e diretório são
representados por instâncias de classes correspondentes, assim como realizado para o padrão
Composite, onde o visitante deverá simplesmente imprimir para cada arquivo e/ou diretório, se o
programa que está realizando o percurso possui permissão de escrita ou não.
Avaliação
1. Na documentação da API de Java lêse para a classe java.util.Currency: “A classe é
projetada de tal forma que não há mais de uma instância de Currency para qualquer que seja a
moeda (currency) considerada. Quais os padrões de projeto que mais diretamente estão associados
a esta afirmação?
2. O padrão Observer é adequadamente empregado em um cenário onde mudanças em um objeto
repercute em vários outros sem que o objeto tenha conhecimento da existência destes outros.
3. O padrão Observer pode ser implementado de várias formas. Em uma delas o que é observado
registra seu interesse em avisar os objetos que precisam ser avisados quando da ocorrência de um
evento significativo.
4. O padrão Observer pressupõe que o Observer notifica seus Observables sem diretamente conhecê
los.
5. O padrão Observer pode ser representado parcialmente usandose a UML onde a classe
Observable é dependente da interface Observer.
6. O padrão Observer estabelece uma dependência do Observer para o Observable.
7. A classe abaixo faz uso de deep copy para implementar o padrão Prototype.
public class X { public Object clone() { return new X(); } }
8. O padrão AbstractFactory faz uso obrigatório do padrão Factory.
9. A ferramenta Jmock faz uso significativo do padrão Prototype.
11. Em uma instituição que instala aparelhos de som em automóveis existem várias configurações
predefinidas para escolha do cliente: mínima, simples, luxo, super e extravagante. Para cada uma
destas configurações temse CaixaSom (representa a marca, quantidade e detalhes das caixas de
som da configuração), AparelhoSom (representa a marca e o modelo do aparelho de som) e assim
por diante. Naturalmente, a elaboração de cada configuração levou em consideração a
compatibilidade entre estes componentes. Há componente que só funciona com outro componente
compatível, ambos parte de uma dada configuração. Qual o padrão de projeto que melhor
expressa este cenário?
12. O emprego do padrão Decorator é uma alternativa para herança. Enquanto herança acrescenta
comportamento em tempo de compilação, um Decorator pode acrescentar funcionalidade em
tempo de execução.
58 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
13. Em uma implementação típica de um Decorator o objeto decorado é fornecido como argumento
ao construtor daquele que decora. Assuma que InputStream é argumento do construtor de
ObjectInputStream. Em conseqüência, este seria um exemplo do emprego do padrão
Decorator.
14. Poder optar pela escolha de um algoritmo a ser empregado em determinado contexto,
possivelmente em tempo de execução, é um exemplo do emprego do padrão Abstract Factory.
15. Uma interface que possui uma operação, digamos, execute(), implementada por várias classes,
cada uma delas explorando uma forma distinta de se realizar a mesma operação é um exemplo do
padrão Strategy.
16. Qual o padrão mais próximo do diagrama incompleto ilustrado abaixo.
17. Qual o padrão mais próximo dos diagramas incompletos ilustrados abaixo.
18. Quando a estrutura de um algoritmo (ou “esqueleto”) é predefinida, mas cujas partes podem ser
implementadas de formas específicas para um contexto, temse o exemplo do emprego do padrão
Template.
19. Um texto típico contém milhares, talvez milhões de caracteres. Representar cada caractere como
um objeto em Java pode ser uma operação de grande consumo de memória. Isto é ainda mais
agravante quando se sabe que os caracteres se repetem e, boa parte empregam o mesmo estilo de
letra. Ou seja, para um texto em uma língua com c caracteres e f fontes diferentes, é necessário
termos c x f objetos, em vez de milhares ou milhões de caracteres. Naturalmente, para cada um
dos c x f objetos teríamos informação específica como a posição do caractere, mas não
repetiríamos nem o caractere nem a fonte por ele empregada. Qual o padrão de projeto que pode
ser empregado neste caso?
59 de 60
Padrões de Projeto: Uma Introdução aos Padrões da “Gangue dos Quatro” Fábio Lucena (INF/UFG)
20. Todo programa em Java pode ser escrito em uma única classe e, neste caso, quanto maior esta
classe pior é o projeto correspondente. Qual o princípio que estaria sendo violado?
Considerações finais
Padrões de projeto são uma ferramenta de projetistas de software. Como toda ferramenta, primeiro é
preciso conhecêla, dominar o uso e identificar o contexto adequado para empregála. Este texto
contribui com estes três objetivos quando a ferramenta é padrão de projeto. Embora o foco tenha se
restringido aos padrões produzidos pela “Gangue dos Quatro”, acreditase que o leitor que tenha
apresentado desenvoltura na resolução dos exercícios aqui fornecidos não terá dificuldades para
compreender outros padrões de projeto.
60 de 60