Prévia do material em texto
Padrões de Projetos de Software com Java
Marcio Quirino - 1
SUMÁRIO
Caminho do Brilho: Saiba Quais Conteúdos Você Irá Aprender Conosco........................................... 8
Onboarding................................................................................................................................................. 8
Padrões GoF de Criação ....................................................................................................................... 9
Descrição ................................................................................................................................................... 9
Propósito .................................................................................................................................................... 9
Preparação ................................................................................................................................................. 9
Introdução .................................................................................................................................................. 9
1. Padrão de projeto Factory Method ........................................................................................ 11
Intenção do padrão Factory Method ......................................................................................................... 11
Problema do padrão Factory Method ................................................................................................... 11
Solução do padrão Factory Method ..................................................................................................... 14
Consequências e padrões relacionados ao Factory Method ................................................................ 16
2. Padrão de projeto Abstract Factory ....................................................................................... 17
Intenção do padrão Abstract Factory ....................................................................................................... 17
Problema do padrão Abstract Factory .................................................................................................. 17
Solução do padrão Abstract Factory .................................................................................................... 19
Consequências e padrões relacionados ao Abstract Factory .................................................................. 22
3. Padrão de projeto Builder ...................................................................................................... 22
Intenção do padrão Builder ...................................................................................................................... 22
Problema do padrão Builder ................................................................................................................. 22
Solução do padrão Builder ................................................................................................................... 23
Consequências e padrões relacionados ao Builder ............................................................................. 26
4. Padrões de projeto Prototype e Singleton ............................................................................. 26
Intenção do padrão Prototype .................................................................................................................. 26
Problema do padrão Prototype ............................................................................................................. 26
Solução do padrão Prototype ............................................................................................................... 28
Consequências e padrões relacionados ao Prototype ......................................................................... 29
Intenção do padrão Singleton................................................................................................................... 30
Problema do padrão Singleton ............................................................................................................. 30
Solução do padrão Singleton ............................................................................................................... 30
Consequências e padrões relacionados ao Singleton .......................................................................... 31
5. Conclusão............................................................................................................................... 32
Considerações Finais ............................................................................................................................... 32
Referências .............................................................................................................................................. 32
Explore+ ................................................................................................................................................... 32
Padrões GoF Estruturais ..................................................................................................................... 34
Descrição ................................................................................................................................................. 34
Propósito .................................................................................................................................................. 34
Preparação ............................................................................................................................................... 34
Padrões de Projetos de Software com Java
Marcio Quirino - 2
Introdução ................................................................................................................................................ 34
1. Padrão de projeto Adapter ..................................................................................................... 35
Intenção do Padrão Adapter..................................................................................................................... 35
Problema Resolvido pelo Padrão Adapter............................................................................................ 35
Solução do Padrão Adapter ................................................................................................................. 37
Consequências e padrões relacionados ao padrão Adapter ................................................................ 39
2. Padrões de projeto Bridge e Decorator ................................................................................. 39
Intenção do padrão Bridge ....................................................................................................................... 39
Problema resolvido pelo padrão Bridge................................................................................................ 40
Solução do padrão Bridge ........................................................................................................................ 42
Consequências e padrões relacionados ao padrão Bridge .................................................................. 43
Intenção do padrão Decorator .................................................................................................................. 44
Problema resolvido pelo padrão Decorator .......................................................................................... 44
Solução do padrão Decorator ............................................................................................................... 45
Consequências e padrões relacionados ao padrão Decorator ............................................................. 47
3. Padrões de projeto Composite e Facade .............................................................................. 48
Intenção do padrãoem PDF
22 // retornar conteúdo da nota no formato PDF
23 }
24
25 private byte[] gerarNotaXLS(NotaNegociacao nota) {
26 // construir cabeçalho em XLS
27 // listar os itens da nota em XLS
28 // gerar sumário em XLS
29 // retornar conteúdo da nota no formato XLS
30 }
31
32 }
A operação exportarNota recebe a nota de negociação a ser exportada e o formato de exportação
(XML, PDF ou XLS).
A solução apresentada não é adequada, pois, além de concentrar em um único módulo todas as
possíveis representações de exportação da nota de negociação, o algoritmo de construção é repetido em
cada formato específico. Além disso, o módulo deve ser modificado a cada nova forma de representação
que for necessária para a nota, violando o princípio Open Closed, um dos princípios SOLID.
Solução do padrão Builder
A solução proposta pelo padrão Builder consiste em separar a criação de objetos complexos de quem
demanda esses objetos, conforme a estrutura definida no diagrama de classes a seguir:
Padrões de Projetos de Software com Java
Marcio Quirino - 24
A interface Builder define as operações que criam as diferentes partes de um produto. Cada forma
particular de criação desse produto é definida em uma classe ConcreteBuilder, que implementa as
operações específicas para a criação das partes definidas na interface Builder.
O participante Director corresponde à classe que constrói o produto utilizando a interface Builder.
Nessa solução, a classe Director fica isolada do conhecimento sobre as diferentes formas de representação
do produto a ser construído.
O diagrama de sequência a seguir ilustra a colaboração entre os participantes do padrão Builder:
Nessa colaboração, o elemento Client representa o objeto que solicita a criação de um produto para
o Director. Para isso, ele cria o Builder específico para o produto desejado (ConcreteBuilder), injetando-o na
instanciação da classe Director.
A partir daí, o objeto Director é responsável por criar as diferentes partes do produto, chamando as
operações específicas do ConcreteBuilder (buildPart_1, buildPart_2 etc.).
• O Builder concreto adiciona as partes solicitadas pelo Director, pois somente ele conhece os
detalhes da representação do produto.
• Ao final, o elemento Client pede o produto construído para o ConcreteBuilder por meio da
operação GetResult.
O diagrama a seguir apresenta a aplicação do padrão Builder no problema apresentado. A classe
ExportadorNota corresponde ao participante Director na estrutura definida pelo padrão. NotaBuilder
representa a interface Builder, enquanto NotaPDFBuilder e NotaXLSBuilder correspondem ao participante
ConcreteBuilder. Cada builder específico constrói uma representação específica do produto (NotaPDF e
NotaXLS).
Padrões de Projetos de Software com Java
Marcio Quirino - 25
O código a seguir ilustra a estrutura da implementação da solução, utilizando o padrão Builder.
A classe ExportadorNota recebe um builder em seu construtor. Esse builder é utilizado no método
exportarNota para gerar as partes que compõem uma nota exportada tanto em PDF quanto em XLS, isto é,
o cabeçalho, os itens negociados e o sumário.
1 public class ExportadorNota {
2 private NotaBuilder builder;
3 public ExportadorNotaNegociacao(NotaBuilder builder) {
4 this.builder = builder;
5 }
6 public void exportarNota(NotaNegociacao nota) {
7 builder.gerarCabecalho(nota);
8 builder.gerarItensNota(nota);
9 builder.gerarSumario(nota);
10 }
11 }
A classe ComandoExportarNotaPDF é um exemplo de cliente do exportador de nota. O método
executar instancia um builder concreto (NotaPDFBuilder), cria um diretor (ExportadorNota), passando o
builder a ser utilizado, e chama a operação de construção do produto desejado (exportarNota). O último
passo é solicitar ao builder concreto o objeto NotaPDF construído.
1 public class ComandoExportarNotaPDF {
2 public NotaPDF executar(NotaNegociacao nota) {
3 NotaPDFBuilder builder = new NotaPDFBuilder();
4 ExportadorNota diretor = new ExportadorNota (builder);
5 diretor.exportarNota(nota);
6 return builder.obterNotaPDF();
7 }
8 }
Padrões de Projetos de Software com Java
Marcio Quirino - 26
Comentário
Note que, nessa solução, o algoritmo geral de exportação é definido apenas na classe ExportadorNota. Além
disso, a estrutura condicional baseada no formato desejado, presente na solução original, não é mais necessária.
Dessa forma, novas representações de exportação da nota de negociação podem ser adicionadas ao sistema,
bastando adicionar novos builders e produtos correspondentes.
Existem algumas questões importantes na implementação desse padrão.
• A primeira é se o objeto Builder deve dar acesso apenas ao produto pronto, isto é, após a
realização de todas as etapas de construção, ou se ele pode dar acesso às partes
intermediárias já construídas do produto.
• No exemplo apresentado, como o objeto que desempenha o papel de Director não precisa
acessar as partes em seu algoritmo de construção, cada Builder concreto fornece acesso
apenas ao produto construído por meio das operações obterNotaPDF e obterNotaXLS.
Entretanto, caso necessário, é admissível que as operações de construção (buildPart_1,
buildPart_2 etc.) retornem uma parte intermediária do produto.
• Outra questão é se os produtos devem ser estruturados em uma hierarquia. No exemplo, as
classes NotaPDF e NotaXLS não foram definidas com uma superclasse comum, pois
assumimos que elas seriam utilizadas de forma bem específica. Entretanto, nada impede que
elas sejam derivadas de uma superclasse ou implementem uma interface genérica.
Consequências e padrões relacionados ao Builder
O padrão Builder é aplicável na construção de objetos complexos e que possam ter diferentes
representações internas. Encapsulando o conhecimento dessas representações em builders concretos que
implementam uma interface genérica comum, os clientes ficam isolados da forma como esses objetos são
internamente construídos.
Atenção
O padrão Abstract Factory, assim como o Builder, pode construir objetos complexos. A diferença principal entre
os dois padrões é que o Builder oferece um mecanismo de construção de um objeto complexo em etapas, enquanto o
foco do Abstract Factory é definir famílias de produtos. Um produto da família é retornado com apenas uma chamada
de operação.
O padrão Composite é utilizado para representar objetos compostos por outros em uma hierarquia
de especializações de um mesmo elemento comum, como ocorre, por exemplo, em uma estrutura de
diretórios e arquivos. O padrão Builder pode ser utilizado para implementar a construção de objetos com
uma estrutura de composição complexa resultante da utilização do padrão Composite.
4. Padrões de projeto Prototype e Singleton
Intenção do padrão Prototype
O padrão Prototype permite a instanciação de objetos a partir da geração de uma cópia de um objeto
protótipo, fazendo com que o módulo cliente não precise conhecer a classe específica que está sendo
instanciada.
Problema do padrão Prototype
Suponha que, no problema apresentado no padrão Abstract Factory, os decodificadores da
mensagem Registrar Cliente, ao invés de criarem apenas objetos da classe MsgRegistrarCliente, tivessem
de criar objetos de classes específicas conforme a origem da mensagem.
Isso significa que a classe RegistrarClienteXMLDecoder, por exemplo, ao invés de criar uma
instância de MsgRegistrarCliente, teria de criar uma instância de MsgRegistrarCliente_X,
Padrões de Projetos de Software com Java
Marcio Quirino - 27
MsgRegistrarCliente_Y ou MsgRegistrarCliente_Z, dependendo da organização origem da mensagem,
imaginando que a validação de cada registro de cliente variasse conforme a organização.
O diagrama a seguir ilustra essa solução. Definimos uma especialização de MsgRegistrarCliente
para cada origem (veja os sufixos X,Y e Z definidos nas subclasses). Cada subclasse implementa um
método específico de validação da mensagem.
Você consegue perceber que essa solução adiciona complexidade ao decodificador?
Veja, no código a seguir, como a classe RegistrarClienteXMLDecoder fica mais complexa, uma vez
que tem de conhecer cada subclasse de MsgRegistrarCliente:
1 public class RegistrarClienteXMLDecoder {
2 public MsgRegistrarCliente decode(String textoMsg, String origem) {
3 MsgRegistrarCliente msg;
4 if (“X”.equals(origem)) {
5 msg = new MsgRegistrarCliente_X();
6 else if (“Y”.equals(origem)) {
7 msg = new MsgRegistrarCliente_Y();
8 else if (“Z”.equals(origem)) {
9 msg = new MsgRegistrarCliente_Z();
10 }
11 // … aqui viria o código de decodificação e preenchimento dos atributos
12 // do objeto MsgRegistrarCliente
13 return msg;
14 }
15 }
Padrões de Projetos de Software com Java
Marcio Quirino - 28
Inserir o processo de decisão sobre o objeto a ser instanciado na implementação do método de
decodificação da mensagem, além de adicionar complexidade, torna a implementação inflexível à adição de
novas origens, pois teríamos de modificar o código inserindo novos comandos condicionais, o que é uma
clara violação do princípio Open Closed, um dos princípios SOLID.
Solução do padrão Prototype
O diagrama a seguir ilustra a estrutura da solução proposta pelo padrão Prototype:
Comentário
A ideia central do padrão é fazer com que uma classe cliente que precise criar instâncias de uma subclasse
específica ou de diferentes subclasses registre uma instância protótipo dessa(s) subclasse(s) e chame a operação
clone do protótipo registrado sempre que precisar de uma nova instância.
A operação clone é definida em cada subclasse e retorna para o módulo cliente uma nova instância
com uma réplica de seu estado. Dessa forma, o módulo cliente não sabe qual subclasse específica foi
instanciada, e novas subclasses podem ser adicionadas ao esquema, sem que o cliente precise ser
modificado.
Você consegue visualizar como ficaria a solução do problema apresentado com a aplicação da
estrutura proposta pelo padrão Prototype?
Em Java, todo objeto já oferece uma implementação padrão para a operação clone, conhecida pelo termo
shallow copy. Essa implementação padrão apenas copia os valores dos atributos de um objeto para sua réplica.
Se um objeto Venda, por exemplo, possuir um atributo que seja uma referência para um objeto Cliente
relacionado, a cópia desse objeto Venda compartilhará com o objeto original a referência para o mesmo objeto Cliente,
ou seja, em uma shallow copy, os objetos referenciados pelo objeto original não são clonados.
Se você precisar criar cópias dos objetos referenciados, deverá criar uma implementação específica da
operação clone, sobrepondo a implementação padrão disponível na classe Object. Esse processo de geração da cópia
de toda a árvore de objetos relacionados ao objeto que está sendo clonado é conhecido pelo termo deep copy.
Em nosso exemplo, vamos utilizar a cópia padrão já oferecida pela classe Object. Portanto, não precisaremos
modificar as subclasses de MsgRegistrarCliente.
O próximo passo é criar e registrar as instâncias protótipo de cada subclasse de MsgRegistrarCliente,
associando-as com a respectiva origem. Faremos isso criando um HashMap e associando o código da origem com a
respectiva instância protótipo, conforme o código a seguir. Antes de criar um decodificador para mensagens XML, a
Padrões de Projetos de Software com Java
Marcio Quirino - 29
fábrica cria as instâncias protótipo de cada subclasse, passando-as para o construtor da classe
RegistrarClienteXMLDecoder.
1 public RegistrarClienteDecoder createRegistrarClienteDecoder() {
2 HashMap‹String, MsgRegistrarCliente›prototypes;
3 prototypes.put(“X”, new MsgRegistrarCliente_X());
4 prototypes.put(“Y”, new MsgRegistrarCliente_Y());
5 prototypes.put(“Z”, new MsgRegistrarCliente_Z());
6
7 return new RegistrarClienteXMLDecoder(prototypes);
8 }
Agora, modificamos a classe RegistrarClienteXMLDecoder, de forma que seu construtor passe a
receber essas instâncias das subclasses de MsgRegistrarCliente, isto é, os protótipos de cada subclasse
associados às respectivas origens. Além disso, substituímos todo o código condicional existente na versão
anterior por uma única chamada à operação clone da instância de MsgRegistrarCliente associada à origem
recebida como parâmetro da operação decode.
1 public class RegistrarClienteXMLDecoder {
2 private HashMap‹String, MsgRegistrarCliente› prototypes;
3
4 public RegistrarClienteXMLDecoder(HashMap‹String, MsgRegistrarCliente›prototypes) {
5 this.prototypes = prototypes;
6 }
7
8 public MsgRegistrarCliente decode(String textoMsg, String origem) {
9 MsgRegistrarCliente prototype = prototypes.get(origem);
10
11 MsgRegistrarCliente msg = (MsgRegistrarCliente) prototype.clone();
12
13 // … aqui viria o código de decodificação e preenchimento dos atributos
14 // do objeto MsgRegistrarCliente
15 return msg;
16 }
Você percebeu que esse código, agora, pode instanciar novas subclasses de MsgRegistrarCliente,
sem que seja necessário modificá-lo?
Nessa solução, a classe MsgRegistrarCliente desempenha o papel de Prototype, e cada subclasse
de MsgRegistrarCliente desempenha o papel de ConcretePrototype. A classe RegistrarClienteXMLDecoder
corresponde ao participante Client definido na estrutura do padrão.
Consequências e padrões relacionados ao Prototype
O padrão Prototype é aplicável em pelo menos três situações específicas:
1. Quando existirem muitas fábricas específicas para a criação de diferentes famílias de
produtos
✓ Esse padrão permite uma solução sem que haja necessidade de implementar uma
subclasse para cada família. Basta definir uma única classe fábrica e criar uma instância
para cada família configurada com os protótipos que serão clonados.
2. Quando as instâncias de uma classe forem resultado de poucas combinações de
estado
✓ Neste caso, é mais simples criar as instâncias típicas a priori e gerar cópias delas, ao
invés de instanciá-las manualmente.
3. Quando o estado de uma classe envolver muitos atributos e relacionamentos com um
processo de criação de novas instâncias muito custoso ou complexo
✓ Neste caso, necessitamos criar objetos com estados idênticos ou com poucas diferenças.
Enquanto o padrão Factory Method define uma hierarquia de classes de criação paralela às classes
produto que são instanciadas, o padrão Prototype substitui essa hierarquia e a chamada a um método fábrica
pelo registro de uma instância protótipo e sua posterior clonagem.
Padrões de Projetos de Software com Java
Marcio Quirino - 30
Atenção
O padrão Prototype permite a criação de fábricas flexíveis que podem ter sua configuração de instâncias
definida e modificada em tempo de execução, ao contrário da solução dada pelo padrão Abstract Factory, que é
estática.
Em contrapartida, o padrão Prototype demanda que cada subclasse do produto a ser instanciado
implemente a operação clone, o que pode ser complexo ou difícil, especialmente nos casos de utilização de
classes de terceiros ou compartilhadas com outros sistemas.
Além disso, os efeitos colaterais oriundos de uma cópia baseada em uma estratégia shallow copy e
a complexidade de implementar uma estratégia deep copy, especialmente quando existir uma árvore
complexa de relacionamentos ou relacionamentos circulares, podem trazer dificuldades à implementação
desse padrão.
Intenção do padrão Singleton
O propósito do padrão Singleton é garantir que exista uma (e apenas uma) instância de uma classe,
provendo um ponto de acesso global a essa instância.
Problema do padrão Singleton
Suponha uma situação na qual você queira garantir que apenas uma instância de uma classe possa
existir em determinado processo, como, por exemplo, no gerenciamentode recursos como cache de objetos,
log, conexões com banco de dados e objetos que representem recursos compartilhados por todo o processo.
Uma possível solução seria definir uma variável global, referenciando o objeto a ser compartilhado.
Dessa forma, todos os módulos que precisassem desse objeto fariam o acesso via essa variável global
compartilhada. O problema é que nada impediria outros módulos de criar múltiplas instâncias dessa classe.
Outra solução seria definir com o escopo de classe todas as operações da classe cujo objeto único
deve ser compartilhado. Em Java, isso significa definir todas as operações da classe com o modificador
static. Essa solução, porém, não é flexível, pois não admite a definição de subclasses e a utilização de
polimorfismo.
Polimorfismo
Princípio do modelo orientado a objetos, pelo qual duas ou mais subclasses de uma mesma superclasse podem
conter métodos com a mesma assinatura, mas com implementações diferentes, resultando em comportamentos
especializados para cada subclasse.
Solução do padrão Singleton
O diagrama a seguir apresenta a estrutura do padrão Singleton.
O nome Singleton representa o nome da classe que você deseja que tenha apenas uma instância.
O atributo unicaInstancia é uma referência a essa única instância a ser compartilhada pelos demais
módulos.
O construtor dessa classe deve ser privativo, garantindo que outros módulos não possam instanciá-
la diretamente. Tanto a operação Instancia quanto o atributo unicaInstancia são propriedades com escopo
de classe (static).
Padrões de Projetos de Software com Java
Marcio Quirino - 31
Um possível uso do padrão Singleton consiste na implementação do padrão Abstract Factory. Veja,
no código a seguir, a implementação de uma fábrica concreta utilizando o padrão Singleton. A instância
compartilhada é referenciada pelo atributo factory, definido com o modificador static. O construtor da classe
é definido como private, o que impede que ela seja diretamente instanciada em outros módulos. A operação
getFactory retorna a instância única compartilhada.
1 public class XMLDecoderFactory extends DecoderFactory {
2 private static DecoderFactory factory = null;
3 private XMLDecoderFactory() {
4 }
5 public static DecoderFactory getFactory() {
6 if (factory == null)
7 factory = new XMLDecoderFactory();
8 return factory;
9 }
10 public abstract RegistrarClienteDecoder createRegistrarClienteDecoder() {
11 return new RegistrarClienteXMLDecoder();
12 }
13 public abstract RegistrarContaDecoder createRegistrarContaDecoder() {
14 return new RegistrarContaXMLDecoder();
15 }
Podemos definir a fábrica abstrata como um registro dos diversos singletons correspondentes às
fábricas concretas.
Veja, no exemplo a seguir, que as fábricas concretas são registradas em um HashMap codificado
pela origem (X, Y ou Z). Cada entrada dessa estrutura de dados associa uma origem ao singleton da
respectiva fábrica concreta. A operação getInstance acessa essa estrutura para retornar a fábrica concreta
correspondente à origem recebida como parâmetro.
1 public abstract class DecoderFactory {
2 private static HashMap‹String, DecoderFactory› factoryMap;
3
4 static {
5 factoryMap = new HashMap‹›();
6 factoryMap.put(“X”, XMLDecoderFactory.getInstance());
7 factoryMap.put(“Y”, CSVDecoderFactory.getInstance());
8 factoryMap.put(“Z”, TextoLivreDecoderFactory.getInstance());
9 }
10
11 public static DecoderFactory getInstance(String origem) {
12 return factoryMap.get(origem);
13 }
14
15 public abstract RegistrarClienteDecoder createRegistrarClienteDecoder();
16 public abstract RegistrarContaDecoder createRegistrarContaDecoder();
17 }
Consequências e padrões relacionados ao Singleton
O padrão Singleton permite o acesso controlado a uma única instância de uma classe, sendo uma
solução superior à utilização de variáveis globais. Permite, inclusive, a criação de subclasses mais
específicas sem impacto para os módulos que utilizam a instância Singleton.
O padrão Singleton é frequentemente utilizado em conjunto com o padrão Abstract Factory, conforme
ilustrado no exemplo anterior.
Padrões de Projetos de Software com Java
Marcio Quirino - 32
Entretanto, após o surgimento de abordagens fortemente baseadas na construção de testes unitários
automatizados e na aplicação de princípios como o da inversão de dependências, o padrão Singleton passou
a ser visto como um potencial problema. Ele pode dificultar a implementação de testes unitários, visto que a
unidade a ser testada pode estar acoplada a Singletons que dificultam o isolamento da unidade em relação
às suas dependências.
Além disso, existem linguagens que permitem quebrar o objetivo original do padrão, pois construções
como reflection e serialização permitem a criação independente de objetos de classes Singleton.
Portanto, esse é um padrão que deve ser utilizado apenas em casos muito específicos para não criar
acoplamentos desnecessários que tornem a estrutura do software menos flexível e dificultem o processo de
testes e depuração dos módulos.
5. Conclusão
Considerações Finais
Neste conteúdo, vimos como os padrões de projeto GoF de criação podem ser usados em soluções
de projeto de software mais flexíveis e menos acopladas.
O padrão Factory Method é baseado em um modelo em que as subclasses implementam uma
interface padrão definida na superclasse para a instanciação dos objetos específicos. Os padrões Abstract
Factory, Builder e Prototype delegam a responsabilidade pela criação de objetos para classes específicas
com essa finalidade. O padrão Abstract Factory sugere a criação de uma hierarquia de fábricas responsável
pela instanciação de uma hierarquia paralela de produtos.
O padrão Builder é aplicável na construção de objetos complexos, compostos por muitas partes e
com um processo de construção custoso e complexo, isolando os módulos clientes dessa complexidade.
O padrão Prototype é baseado na geração de cópias de objetos protótipos pré-fabricados e mais
voltado para a composição de objetos prontos, ao contrário do Abstract Factory, que é baseado em uma
estrutura estática de hierarquia de classes.
Por fim, o padrão Singleton já foi bastante utilizado, mas, atualmente, é considerado por muitos um
antipadrão, isto é, uma solução inadequada e que deve ser evitada, com exceção de situações muito
específicas de gerenciamento de recursos que não podem ser utilizados de forma simultânea.
Referências
GAMMA, E.; HELM, R.; JOHNSON, R.; VLISSIDES, J. Design Patterns: Elements of Reusable
Object-Oriented Software. 1. ed. Boston: Addison-Wesley, 1994.
MARTIN, R. C. Clean Architecture: A Craftsman´s Guide to Software Structure and Design. 1. ed.
Upper Saddle River, NJ: Prentice Hall, 2017.
METSKER, S. J.; WAKE, W. C. Design Patterns in Java. 1.ed. Boston: Addison-Wesley, 2006.
Explore+
Para saber mais sobre a programação orientada a objetos, acesse o site da DevMedia e leia o artigo
intitulado Utilização dos princípios SOLID na aplicação de padrões de projeto.
O site “Padrões de projeto/Design patterns – Refactoring.Guru” apresenta um conteúdo interativo e
bastante completo de todos os padrões GoF com exemplos de código em diversas linguagens de
programação.
Padrões de Projetos de Software com Java
Marcio Quirino - 33
Além dos padrões GoF tradicionais, outros padrões voltados para o desenvolvimento de aplicações
corporativas em Java EE podem ser encontrados no livro Java EE 8 Design Patterns and Best Practices,
escrito por Rhuan Rocha e João Purificação. A obra aborda padrões para interface com o usuário, lógica do
negócio, integração de sistemas, orientação a aspectos, programação reativa e microsserviços.
Padrões de Projetos de Software com Java
Marcio Quirino - 34
Padrões GoF Estruturais
Descrição
Apresentação de padrões GoF de projeto estruturais: Adapter,Bridge, Decorator, Composite,
Facade, Flyweight, Proxy.
Propósito
Compreender os padrões de projeto GoF ligados à estruturação de objetos e identificar
oportunidades para a sua aplicação na programação orientada a objetos são habilidades importantes para
um projetista de software, pois, sem elas, as soluções geradas podem se tornar inflexíveis e dificultar a
evolução dos sistemas em prazo e custo aceitáveis.
Preparação
Antes de iniciar o conteúdo, é recomendado instalar em seu computador um programa que permita
elaborar modelos sob a forma de diagramas da UML (Linguagem Unificada de Modelagem). Nossa sugestão
inicial é o Free Student License for Astah UML, usado nos exemplos deste estudo. Para isso, será necessário
usar seu e-mail institucional para ativar a licença.
Preencha os dados do formulário no site do software, envie e aguarde a liberação de sua licença em
seu e-mail institucional. Ao receber a licença, siga as instruções do e-mail e instale o produto em seu
computador. Os arquivos Astah com diagramas UML utilizados neste conteúdo estão disponíveis para
download.
Sugestões de links adicionais de ferramentas livres para modelagem de sistemas em UML (UML
Tools) podem ser encontradas em buscas na Internet.
Além disso, recomendamos a instalação de um ambiente de programação em Java. O ambiente
recomendado para iniciantes em Java é o Apache Netbeans, cujo instalador pode ser encontrado no site do
ambiente, acessando o menu Download. Porém, antes de instalar o Netbeans, é necessário ter instalado o
JDK (Java Development Kit) referente à edição Java SE (Standard Edition), que pode ser encontrado no site
da Oracle Technology Network: Java SE - Downloads | Oracle Technology Network | Oracle.
Introdução
Os padrões GoF, do inglês “Gang of Four”, são padrões de projeto orientados a objetos divididos em
três categorias: de criação, estruturais e comportamentais. São assim denominados por terem sido
introduzidos pelos quatro autores do livro Design Patterns: Elements of Reusable Object-Oriented Software
(GAMMA et al., 1994). Os padrões de projeto GoF estruturais descrevem estratégias para compor classes
e objetos de modo a compor estruturas maiores, que podem ser formadas com soluções baseadas em
herança ou em composição de objetos. Neste conteúdo, você aprenderá os sete padrões deste grupo:
Adapter, Bridge, Decorator, Composite, Facade, Flyweight e Proxy.
Padrões de Projetos de Software com Java
Marcio Quirino - 35
O padrão Adapter permite que módulos de um sistema possam interagir com diferentes
implementações de um mesmo serviço por meio de uma interface genérica. O padrão Bridge separa a
abstração de um objeto da sua implementação, de modo que ambos possam variar de forma independente.
O padrão Decorator descreve uma maneira de adicionar, dinamicamente, responsabilidades a objetos por
meio de estruturas de composição, em vez de soluções baseadas em herança. O padrão Composite
descreve como podemos estruturar uma hierarquia de objetos construída a partir de dois tipos de elementos:
primitivos e compostos. O padrão Facade propõe a criação de uma interface de alto nível para um
subsistema, de modo a isolar os módulos clientes do conhecimento da estrutura interna desse subsistema.
O padrão Flyweight apresenta uma solução para o problema de compartilhamento de um número
elevado de pequenos objetos, com o objetivo de utilizar os recursos de memória de forma mais racional. Por
fim, o padrão Proxy consiste na criação de um objeto substituto do real destino de uma requisição, sendo
útil para abstrair a complexidade na comunicação entre objetos distribuídos ou para lidar com a criação sob
demanda de um objeto com grande consumo de recursos, como imagens, por exemplo.
1. Padrão de projeto Adapter
Intenção do Padrão Adapter
Adapter é um padrão cujo propósito é converter a interface de uma classe existente em outra
interface esperada pelos módulos clientes.
O padrão Adapter é especialmente útil quando estamos desenvolvendo um sistema que precisa
interagir com um serviço implementado por terceiros, que forneçam componentes e interfaces diferentes e
incompatíveis entre si.
Problema Resolvido pelo Padrão Adapter
• Imagine um turista que esteja viajando do Brasil para os EUA e depois para alguns países da
Europa.
• Ele certamente vai precisar carregar o seu telefone celular durante a viagem, não é mesmo?
• Entretanto, o padrão de tomada brasileiro é diferente do norte-americano e do europeu. O que
ele precisa levar na bagagem para poder plugar o seu carregador em tomadas com diferentes
padrões?
• Isso mesmo! Ele vai precisar de um ou mais adaptadores de tomada.
Mas qual é a relação entre tomadas e o desenvolvimento de software?
Padrões de Projetos de Software com Java
Marcio Quirino - 36
• Suponha que você esteja desenvolvendo um sistema para vendas on-line de diferentes
varejistas, sendo que cada loja pode utilizar uma ou mais das soluções de pagamento disponíveis
no mercado.
• Cada fornecedor de solução de pagamento permite, por exemplo, a realização de uma transação
em cartão de crédito, por meio de uma chamada à sua API de pagamento.
• Portanto, o software de vendas deve ser capaz de ser plugado a diferentes APIs de pagamento
que oferecem basicamente o mesmo serviço: intermediar as diversas formas de pagamento
existentes no mercado.
• O problema é que cada API tem uma interface proprietária definida pelo fornecedor.
Gostaríamos que nosso software de vendas pudesse trabalhar com novos fornecedores que
apareçam no mercado, sem que seja necessário alterar módulos já existentes, mas apenas adicionando
novos módulos.
O código a seguir apresenta um exemplo hipotético e simplificado de duas soluções de pagamento
disponíveis no mercado.
A. A primeira oferece uma classe de nome PagXPTO com duas operações distintas para o
pagamento, conforme a bandeira do cartão (Visa ou Mastercard).
B. A segunda fornece a classe ABCPagamentos com apenas uma operação de pagamento.
Perceba que, além de nomes distintos, os parâmetros dessas operações também são diferentes.
// Solução de pagamentos do fornecedor XPTO
public class PagXPTO {
public void pagarCartaoVisa (DadosCartao cartao, BigDecimal valor);
public void pagarCartaoMastercard (DadosCartao cartao, BigDecimal valor);
}
// Solução de pagamentos do fornecedor ABC
public class ABCPagamentos {
public void pagarEmCartaoCredito (String numeroCartao, String nome, String CVV, String
validade, BigDecimal valor);
}
Sem a utilização do padrão Adapter, um módulo cliente que precisasse chamar uma API de
pagamento poderia ser implementado da forma esquemática ilustrada pelo código a seguir.
public class ServicoPagamento {
// o parâmetro nomeBroker define a API a ser chamada: XPTO ou ABC
public void pagarCartaoCredito (String nomeBroker, CartaoCredito cartao, BigDecimal
valor) {
if (“XPTO”.equals(nomeBroker)) {
pagarCartaoXPTO(cartao, valor);
} else if (“ABC”.equals(nomeBroker)) {
pagarCartaoABC(cartao, valor);
}
}
// chamada à API de pagamento do fornecedor XPTO
private void pagarCartaoXPTO(CartaoCredito cartao, BigDecimal valor) {
PagXPTO brokerPagamento = new PagXPTO();
// converte o parâmetro cartao para a estrutura requerida pela API
DadosCartao dadosCartao = converterCartao(cartao);
// com base no número do cartão, define a função da API a ser chamada
if (isCartaoVisa(cartao)) {
brokerPagamento.pagarCartaoVisa(dadosCartao, valor);
} else {
brokerPagamento.pagarCartaoMastercard(dadosCartao, valor);
}
}
// chamada à API de pagamento do fornecedor ABC
private void pagarCartaoABC(CartaoCredito cartao, BigDecimal valor) {
ABCPagamentos brokerPagamento = new ABCPagamentos();
Padrões de Projetos de Software com Java
Marcio Quirino - 37
brokerPagamento.pagarEmCartaoCredito(cartao.numero(), cartao.nome(),
cartao.cvv(), cartao.validade(),valor);
}
}
Note que, como cada API possui uma interface específica, o código para a realização do pagamento
em cartão de crédito precisa ser específico para cada fornecedor, sendo implementado pelos métodos
pagarCartaoXPTO e pagarCartaoABC.
Você consegue visualizar como ficaria esse código com dezenas de fornecedores e de operações
além do pagamento em cartão de crédito?
O resultado seria um código enorme, complexo e teria que ser modificado a cada nova operação ou
fornecedor.
Portanto, na analogia com o carregador de telefone celular, o módulo cliente corresponde ao
carregador, enquanto os fornecedores ABC e XPTO correspondem aos diferentes tipos de tomada.
Precisamos de uma solução que nos permita conectar o módulo cliente com qualquer fornecedor de serviço
de pagamento.
Solução do Padrão Adapter
O Adapter é um padrão cuja estrutura é apresentada no diagrama UML a seguir.
O participante Client representa um módulo cliente, enquanto Adaptee representa uma
implementação específica do serviço. Em vez de o módulo Client utilizar diretamente essa implementação
específica, o padrão sugere o uso de uma abstração, representada pelo participante Target, que define uma
interface comum a todas as implementações.
Comentário
Para cada Adaptee específico, deve ser criado um adaptador, representado pelo participante Adapter, que
traduz a chamada genérica definida na interface Target (operation) em uma ou mais chamadas da respectiva
implementação específica (specificOperation).
A imagem a seguir ilustra a aplicação desse padrão no exemplo dos fornecedores de API de
pagamentos.
Padrões de Projetos de Software com Java
Marcio Quirino - 38
Nesse exemplo, cada fornecedor (ABCPagamentos e PagXPTO) corresponde a uma ocorrência do
participante Adaptee, definindo uma API específica que não pode ser alterada.
A interface BrokerPagamento representa a generalização dos serviços oferecidos pelos diferentes
fornecedores e corresponde ao participante Target do padrão.
Os demais módulos do sistema utilizarão apenas essa interface, ficando isolados do fornecedor
concreto da implementação desses serviços. BrokerABCAdapter e BrokerXPTOAdapter são classes que
representam o papel Adapter do padrão e são responsáveis por traduzir a interface genérica definida em
BrokerPagamento em chamadas específicas definidas pelos fornecedores ABCPagamentos e PagXPTO,
respectivamente.
Comentário
Cada fornecedor possui um adaptador específico, assim como existem adaptadores específicos para cada
padrão de tomada elétrica.
A implementação a seguir mostra a estrutura da implementação com a utilização do padrão.
A classe ServicoPagamento, que representa um módulo cliente de uma solução de pagamento,
recebe um objeto (broker) que implementa a interface genérica de pagamento (BrokerPagamento), ficando,
portanto, isolada das implementações específicas. Novas soluções de pagamento podem ser acrescentadas
ao produto sem haver necessidade do módulo cliente ser alterado.
Para cada fornecedor de API, foi criada uma classe Adapter que implementa o mapeamento da
interface genérica nas chamadas específicas da respectiva API.
Observe como o código, que na solução anterior estava todo concentrado no módulo cliente, deu
origem aos adaptadores para cada fornecedor de API de pagamento.
public class ServicoPagamento {
// o parâmetro BrokerPagamento é um objeto (adapter) que implementa a interface
genérica
// BrokerPagamento
public void pagarCartaoCredito (BrokerPagamento broker, CartaoCredito cartao,
BigDecimal valor) {
broker.pagarCartaoCredito(cartao, valor);
}
}
Padrões de Projetos de Software com Java
Marcio Quirino - 39
public class BrokerXPTOAdapter implements BrokerPagamento (
public void pagarCartaoCredito(CartaoCredito cartao, BigDecimal valor) {
PagXPTO brokerPagamento = new PagXPTO();
DadosCartao dadosCartao = converterCartao(cartao);
if (isCartaoVisa(cartao)) {
brokerPagamento.pagarCartaoVisa(dadosCartao, valor);
} else {
brokerPagamento.pagarCartaoMastercard(dadosCartao, valor);
}
}
}
public class BrokerABCAdapter implements BrokerPagamento {
public void pagarCartaoCredito(CartaoCredito cartao, BigDecimal valor) {
ABCPagamentos brokerPagamento = new ABCPagamentos();
brokerPagamento.pagarEmCartaoCredito(cartao.numero(), cartao.nome(),
cartao.cvv(), cartao.validade(), valor);
}
}
Consequências e padrões relacionados ao padrão Adapter
O padrão Adapter permite incorporar módulos previamente desenvolvidos, modificando sua interface
original, sem que haja necessidade de alterá-los. Esse tipo de solução possibilita a utilização de diferentes
implementações proprietárias compartilhando uma única interface, isolando, portanto, os módulos clientes
das diferentes interfaces proprietárias.
O esforço envolvido na implementação de um Adapter depende do grau de semelhança que exista
entre a interface genérica Target e a interface específica do componente para o qual o Adapter esteja sendo
construído. Quanto maior a semelhança, menor o esforço e vice-versa.
Comentário
O participante Target pode ser definido como uma interface puramente abstrata ou como uma classe abstrata,
caso os diversos adaptadores possuam métodos em comum ou as operações compartilhem uma sequência comum
de passos, em que cada passo tenha uma implementação específica em cada adaptador.
Neste último caso, portanto, as operações com uma sequência comum seriam implementadas na
superclasse da qual os adaptadores específicos herdariam, aplicando-se conjuntamente o padrão Template
Method.
Comentário
O padrão Bridge tem uma estrutura similar ao padrão Adapter, porém, com um objetivo diferente, que é separar
a interface da sua implementação, de modo que elas possam mudar de forma independente, sendo aplicado quando
estamos implementando tanto os módulos clientes, como os fornecedores. O padrão Adapter, por sua vez, visa oferecer
uma diferente interface para um ou mais componentes existentes, tipicamente fornecidos por terceiros.
2. Padrões de projeto Bridge e Decorator
Intenção do padrão Bridge
O objetivo principal do padrão Bridge é desacoplar uma abstração da sua implementação, permitindo
que ambas possam variar de forma independente.
O padrão Bridge é, tipicamente, utilizado para evitar um problema de explosão combinatória de
classes, resultante de soluções baseadas em herança, especialmente em hierarquias que precisam
combinar aspectos do domínio (abstração) com outros específicos de implementação, tais como
plataformas, tecnologias, estruturas de dados etc.
Padrões de Projetos de Software com Java
Marcio Quirino - 40
Problema resolvido pelo padrão Bridge
Normalmente, quando uma abstração pode ter diferentes implementações com código em comum
entre elas, a solução costuma ser a definição de uma hierarquia na qual o código comum é estabelecido na
superclasse abstrata e cada implementação específica é definida em uma subclasse concreta.
Entretanto, esse tipo de solução não é flexível para comportar diferentes classificações da mesma
abstração, pois cada classificação demanda uma hierarquia própria. O resultado é que a combinação dessas
diferentes classificações por meio de herança gera um número enorme de classes, tornando o projeto
complexo e inflexível.
Esse problema costuma ocorrer quando combinamos diferentes perspectivas de classificação de
uma abstração.
Exemplo
Podemos combinar a perspectiva do domínio com a de plataforma, como ocorre na implementação de
frameworks de interface gráfica com o usuário, pois os elementos visuais, como painéis, botões e caixas de texto
correspondem às abstrações que precisam ser implementadas em diferentes plataformas, tais como Windows, Motif
etc.
A imagem a seguir apresenta um exemplo hipotético de um componente textual (TextComponent),
que possui uma implementação específica para Windows e outrapara Motif.
Você consegue perceber que o aspecto de variação entre as duas subclasses de
TextComponent é a plataforma?
A solução seria aceitável se houvesse apenas essa perspectiva de variação. Porém, podem existir
especializações de um componente textual inerentes ao domínio da abstração, tais como uma área de texto
(TextArea) e um campo de texto (TextField), conforme ilustrado pelo diagrama a seguir.
Padrões de Projetos de Software com Java
Marcio Quirino - 41
Você consegue visualizar que existem especializações do componente texto próprias do
domínio de interface gráfica, ao mesmo tempo em que existem especializações do componente texto
decorrentes das diferentes plataformas de implementação da interface gráfica?
O problema agora é que tanto um TextField quanto um TextArea precisam ser implementados nas
duas plataformas: Windows e Motif.
Qual seria a solução mais adequada?
Uma solução baseada na combinação dessas classificações é ilustrada no diagrama UML a seguir.
Comentário
Perceba que esse caminho de solução gera uma enorme combinação de classes, misturando aspectos do
domínio com aspectos de implementação e resultando em uma estrutura complexa e inflexível.
Portanto, o problema que o padrão Bridge soluciona é a separação da abstração (TextComponent e
suas especializações do domínio, TextArea e TextField) dos aspectos de sua implementação (plataformas
Padrões de Projetos de Software com Java
Marcio Quirino - 42
Windows e Motif), permitindo que ambas possam evoluir separadamente, isto é, podemos adicionar novos
tipos de componentes e novas plataformas sem gerar um problema combinatório de difícil gestão.
Solução do padrão Bridge
A estrutura da solução proposta pelo padrão Bridge está representada no diagrama de classes a
seguir:
Comentário
A ideia proposta pelo padrão é separar a hierarquia de abstrações da hierarquia de implementações, fazendo
com que a implementação das operações na abstração (participante Abstraction) seja responsável por delegar a
execução para a implementação específica que estiver conectada à abstração (participante ConcreteImplementor).
Veja na imagem a seguir como esse padrão é aplicado na implementação do Java AWT (Abstract
Window Toolkit), framework básico de componentes de interface gráfica com o usuário da linguagem Java.
Perceba que a hierarquia à esquerda corresponde à abstração dos componentes do domínio de
interface gráfica definida pelo participante Abstraction na estrutura do padrão Bridge, enquanto a hierarquia
à direita corresponde aos aspectos de implementação dessa abstração nas diversas plataformas.
Padrões de Projetos de Software com Java
Marcio Quirino - 43
Atenção
Cada componente na abstração de elementos visuais do AWT possui uma ponte para o elemento peer
correspondente, sendo que cada elemento folha da hierarquia peer possui implementação específica para uma
plataforma.
Desse modo, um componente TextArea, por exemplo, possui uma operação insert, cuja
implementação é responsável por chamar a operação insert do elemento peer associado. Neste caso, o
componente TextAreaPeer possui duas implementações específicas: uma implementação Windows
(WTextAreaPeer) e uma implementação Motif (MTextAreaPeer).
O código a seguir é um extrato da implementação desses componentes no AWT.
Comentário
Note que o método insertText, chamado pela operação insert da classe TextArea, acessa o elemento peer
definido no ancestral Component e faz o downcasting desse elemento para TextAreaPeer, pois esse elemento é
definido com o tipo genérico ComponentPeer. Na sequência, a operação insert do peer associado é chamada.
Desse modo, a implementação do componente TextArea não tem qualquer menção à plataforma
específica. A classe WTextAreaPeer, por sua vez, implementa a operação insert chamando um código nativo
para Windows, que atualizará o texto do componente na interface gráfica do Windows.
public class TextArea extends TextComponent {
public void insert (String str, int pos) {
insertText (str, pos);
}
public void insertText (String str, int pos) {
// insere o texto na posição indicada
String tmp1 = getText().substring(0, pos);
String tmp2 = getText().substring(pos, getText().length());
// chama o método setText da superclasse para atualizar o novo conteúdo
setText (tmp1 + str + tmp2);
// obtém o elemento peer associado específico de plataforma
TextAreaPeer peer = (TextAreaPeer) super.getPeer();
// chama a operação do componente peer para renderizar o texto na plataforma
alvo
if (peer != null) {
peer.insert(str, pos);
}
}
}
// classe TextArea na plataforma alvo Windows
public class WTextAreaPeer implements TextAreaPeer {
public void insert(String text, int pos) {
replaceRange(text, pos, pos);
}
// código nativo específico da plataforma Windows
public native void replaceRange(String text, int start, int end);
}
Consequências e padrões relacionados ao padrão Bridge
O padrão Bridge possibilita o desacoplamento de uma hierarquia de abstrações em relação a uma
hierarquia de implementações associadas, o que promove a extensibilidade da solução, pois podemos
acrescentar, de forma independente, tanto novos itens à abstração quanto novas implementações
associadas.
Os objetos da hierarquia de implementação precisam ser criados e plugados nas respectivas
abstrações.
Para promover o isolamento entre as abstrações e as suas implementações específicas, o padrão
Abstract Factory pode ser utilizado para criar e configurar uma ponte entre as duas hierarquias.
Padrões de Projetos de Software com Java
Marcio Quirino - 44
Comentário
A estrutura de solução do padrão Bridge é similar à do padrão Adapter. A diferença fundamental é que o padrão
Adapter é aplicado quando desejamos incorporar à solução elementos de terceiros, já prontos, que não podem ser
modificados e que precisam ser acessados a partir de uma interface comum. Desse modo, no padrão Adapter, a
interface comum é tipicamente definida a posteriori, ou seja, a partir de implementações já existentes que possuem
interfaces distintas e incompatíveis entre si. O padrão Bridge, por outro lado, é uma solução definida a priori, ou seja,
a interface comum de implementação é determinada de forma a permitir que diferentes implementações possam ser
construídas a partir da sua definição.
Intenção do padrão Decorator
Decorator é um padrão que permite adicionar responsabilidades a um objeto de forma dinâmica e
mais flexível do que utilizando subclasses.
Problema resolvido pelo padrão Decorator
Uma forma comum de estender a funcionalidade de classes é por meio da criação de subclasses.
Exemplo
Imagine que você queira adicionar às classes de leitura e escrita de arquivos texto a capacidade de criar e ler
arquivos texto criptografados e compactados.
Poderíamos resolver esse problema por meio de herança das classes do pacote Java I/O, criando
uma estrutura como a ilustrada no código a seguir. O pacote Java I/O define a classe FileWriter, que oferece
operações para a escrita de arquivos texto. Definimos, então, a classe EncryptedFileWriter como uma
subclasse de FileWriter e sobrescrevemos o método write, adicionando a capacidade de criptografar o texto
antes que seja escrito no arquivo.
public class EncryptedFileWriter extends FileWriter {
public EncryptedFileWriter(File file) throws IOException {
super(file);
}
public void write(String text) throws IOException {
String encryptedText = encrypt(text);
super.write(encryptedText); // comanda a gravação em disco via FileWriter
}
private String encrypt(String text) {
String result = text;
// aqui estaria o código para encriptar o texto
return result;
}
}
Para adicionar a capacidade de criar arquivos texto compactados, podemos criar uma outra extensão
da classe FileWriter, denominada CompressedFileWriter, sobrescrevendoos métodos write e close. Nessa
solução simplificada, supomos que a compactação ocorre somente no momento do fechamento do arquivo.
public class CompressedFileWriter extends FileWriter {
StringBuilder buffer = new StringBuilder();
public CompressedFileWriter(File file) throws IOException {
super(file);
}
public void write(String text) throws IOException {
buffer.append(text);
}
public void close() throws IOException {
// compacta o conteúdo no instante em que o arquivo vai ser fechado
char[] compressedBuffer = compress(buffer.toString());
super.write(compressedBuffer); // comanda a gravação em disco via FileWriter
super.close();
Padrões de Projetos de Software com Java
Marcio Quirino - 45
}
private char[] compress(String buffer) {
// algoritmo de compressão implementado aqui
// retorna resultado da compressão em um array
}
}
Soluções baseadas em herança para problemas dessa natureza são pouco flexíveis.
Você consegue identificar por que a solução dada para a criptografia e a compressão de arquivos
texto nos exemplos anteriores é pouco flexível?
Agora, imagine que você queira gerar um arquivo texto criptografado e compactado. Poderíamos
definir a classe CompressedFileWriter como uma subclasse da classe EncryptedFileWriter, em vez de
FileWriter. Porém, cairíamos em outra situação problemática, pois não poderíamos gerar um arquivo apenas
compactado e sem criptografia, a menos que adicionássemos complexidade ao projeto por meio de
parâmetros de controle como setCryptoON, setCryptoOFF, por exemplo.
Portanto, o problema resolvido pelo padrão Decorator consiste em adicionar novos comportamentos
às funcionalidades já existentes em uma determinada classe, sem que seja necessário alterar o código
dessa classe ou criar subclasses, de modo que esses elementos possam ser combinados de maneiras
diferentes, conferindo flexibilidade à solução.
Solução do padrão Decorator
A estrutura da solução proposta pelo padrão Decorator está representada no diagrama de classes a
seguir:
O participante Component define uma interface implementada pelas classes concretas,
correspondentes ao participante ConcreteComponent, que podem ter comportamentos adicionados a elas
dinamicamente. O participante Decorator também é uma especialização de Component e, portanto, todas
as operações definidas em Component podem ser sobrescritas pelos elementos representados pelo
participante Decorator. Além disso, comportamentos adicionais são definidos nas implementações concretas
do Decorator, representadas pelos participantes ConcreteDecoratorA e ConcreteDecoratorB.
Padrões de Projetos de Software com Java
Marcio Quirino - 46
Atenção
Cada Decorator tem uma referência para o elemento Component, ao qual ele está adicionando funcionalidades,
funcionando como uma espécie de envoltório ao componente original.
As classes do pacote Java I/O utilizam amplamente essa solução, conforme ilustrada por alguns
elementos presentes no diagrama UML, a seguir:
• Writer é uma classe abstrata que corresponde ao participante Component do padrão.
• As classes OutputStreamWriter e FileWriter desempenham o papel de ConcreteComponent e
implementam as operações de escrita em um arquivo texto.
• As classes BufferedWriter e PrintWriter correspondem ao participante ConcreteDecorator,
adicionando funcionalidades a algum objeto que implemente a definição da classe Writer.
Note que os desenvolvedores optaram por não implementar uma superclasse comum no papel de
Decorator, fazendo com que ambas as classes tenham uma referência para o Writer que está sendo
decorado.
A classe BufferedWriter adiciona a um Writer a capacidade de manter uma área de armazenamento
interno em memória e somente comandar a escrita em disco quando essa área estiver cheia, reduzindo o
número de acessos a disco. A classe PrintWriter adiciona funcionalidades de saída formatada por meio de
operações como printf e println.
O código a seguir apresenta um trecho da implementação da classe BufferedWriter, utilizando o
padrão Decorator.
Todo Decorator recebe, no seu construtor, um parâmetro correspondente ao participante
Component do padrão, que, neste exemplo, corresponde à classe abstrata Writer.
A classe BufferedWriter implementa uma versão específica decorada da operação write que
armazena os dados recebidos em um array e, somente quando a capacidade ultrapassa um limiar, ela
chama a operação localFlush, que se responsabiliza por chamar a operação write do componente que está
sendo decorado, fazendo efetivamente a escrita da área temporária para o arquivo.
Padrões de Projetos de Software com Java
Marcio Quirino - 47
public class BufferedWriter extends Writer
private Writer out; // elemento que está sendo decorado
char[] buffer; // área de escrita temporária em memória
int count; // número de caracteres ocupados na área de escrita temporária
public BufferedWriter(Writer out) throws IOException {
this.out = out;
buffer = new char[4096]; // cria uma área temporária de 4K
}
public void write(char[] buf, int offset, int len) throws IOException {
if (count + len > buffer.length) { // se a área a ser escrita não couber mais
no buffer
localFlush(); // descarrega área para disco
} else {
System.arraycopy(buf, offset, buffer, count, len); // salva na área
temporária
count += len; // incrementa espaço ocupado
if (count == buffer.length) // chegou no limite da área temporária?
localFlush(); // descarrega área para disco
}
}
public void flush() throws IOException {
localFlush();
out.flush();
}
protected void localFlush() throws IOException {
if (buffer.length > 0)
out.write(buffer, 0, count); // chama operação write do componente
decorado
}
}
O código a seguir mostra como essas classes podem ser utilizadas em conjunto. Note como o objeto
writer é decorado por um BufferedWriter e por um PrintWriter, que adicionam as funcionalidades de
armazenamento em área temporária e saída formatada, respectivamente, ao objeto FileWriter base. Em vez
de herança, um esquema de composição sucessiva é utilizado, com o objeto base sendo passado como
parâmetro para o construtor do elemento que adiciona novas funcionalidades. Desse modo, um objeto
FileWriter é passado ao construtor da classe BufferedWriter que, por sua vez, é passado ao construtor da
classe PrintWriter.
public class Exemplo {
public static void main(String[] args) {
try (FileWriter writer = new FileWriter("app. log");
BufferedWriter bw = new BufferedWriter(writer); // writer é o objeto
base
PrintWriter pw = new PrintWriter(bw)) { // bw é o objeto base
pw.println("Exemplo de uso do decorator");
} catch (IOException e) {
System.err.format("IOException: %s%n", e);
}
}
}
Consequências e padrões relacionados ao padrão Decorator
O padrão Decorator oferece uma forma mais flexível de adicionar funcionalidades a objetos do que
as soluções baseadas em herança, por ser baseado em composição de objetos. Com a utilização desse
padrão, responsabilidades podem ser adicionadas, em tempo de execução, pela adição de camadas de
decoradores, formando uma estrutura similar a uma cebola.
Além disso, a solução baseada em composição possibilita a definição de uma interface mais simples
para o objeto base (o núcleo da cebola), ao contrário das soluções baseadas em herança, em que
Padrões de Projetos de Software com Java
Marcio Quirino - 48
frequentemente a superclasse tem que ser estendida para comportar novas operações definidas nas
subclasses.
Entretanto, as soluções que utilizam o padrão Decorator podem acabar utilizando muitos objetos
pequenos, todos muito parecidos e conectados, o que pode complicar o seu entendimento e a correção de
erros.
O padrão Decorator tem estrutura similar à utilizada no padrão Composite, mas um decorator tem o
propósito de adicionar responsabilidadesa outros objetos, enquanto o padrão Composite está mais ligado
à agregação de objetos em uma estrutura hierárquica.
Desafio
Você consegue implementar as classes EncryptedFileWriter e CompressedFileWriter apresentadas
anteriormente utilizando o padrão Decorator?
3. Padrões de projeto Composite e Facade
Intenção do padrão Composite
O padrão Composite permite representar hierarquias de composição de objetos em uma estrutura
de árvore, viabilizando que a manipulação dos objetos individuais e dos agregados possa ser feita com o
mesmo conjunto de operações.
Problema resolvido pelo padrão Composite
Exemplo
Suponha que você esteja desenvolvendo um programa de envio e recepção de mensagens texto que devem
ser organizadas em pastas. Além de mensagens, pastas podem conter outras pastas.
Podemos realizar operações com as mensagens, tais como apagar uma mensagem, criptografar
uma mensagem, entre outras. Essas mesmas operações se aplicam às pastas, sendo que apagar uma
pasta implica em apagar todas as mensagens e as pastas descendentes dela.
O padrão Composite é especialmente interessante para problemas de representação de hierarquias
de elementos nas quais observamos recursividade na execução das operações aplicadas aos agregados.
O código apresentado a seguir ilustra a estrutura de implementação para o problema sem utilizar o
padrão Composite.
public class ServicoMensagem {
public void apagar(Object elemento) {
if (elemento instanceof Mensagem)
apagarMensagem((Mensagem) elemento);
else if (elemento instanceof Pasta)
apagarPasta((Pasta) elemento);
}
private void apagarMensagem(Mensagem mensagem) {
// lógica para apagar a mensagem
}
private void apagarPasta(Pasta pasta) {
// lógica para apagar a pasta
// que replicaria a lógica condicional da operação apagar
// pois uma pasta pode conter mensagens e pastas
}
}
Esse código apresenta problemas, pois, além das operações com os diferentes elementos estarem
concentradas em uma única classe (ServicoMensagem), existe um código condicional baseado no tipo do
objeto (mensagem ou pasta), que identifica a operação a ser executada (apagarMensagem ou apagarPasta).
Padrões de Projetos de Software com Java
Marcio Quirino - 49
A adição de novas operações e novos tipos de objeto, certamente, aumentará a complexidade dessa
implementação.
Solução do padrão Composite
A estrutura da solução proposta pelo padrão Composite está representada no diagrama de classes
a seguir:
A ideia central do padrão é representar a hierarquia de modo que todos os elementos herdem de
uma superclasse genérica representada pelo participante Component.
• O participante Leaf representa um elemento primitivo da hierarquia (folha), isto é, um
elemento que não possui descendentes, enquanto o participante Composite representa os
agrupamentos de elementos (folhas ou outros agregados).
• O agrupamento é definido pelo relacionamento entre o agregado Composite e seus filhos do
tipo Component.
• O participante Client representa um módulo cliente qualquer que utiliza os elementos da
hierarquia.
• Nesse modelo, operation representa cada operação aplicável tanto à folha como ao
agregado.
• Além disso, um Composite define a implementação para operações de gerenciamento do
agregado, permitindo adicionar e remover elementos, além de obter um elemento específico.
A estrutura de código a seguir ilustra a aplicação do padrão no exemplo do programa de mensagens.
public abstract class Elemento {
public void adicionar(Elemento elem) { }
public void remover(Elemento elem) { }
public abstract void apagar();
public abstract void criptografar();
}
public class Mensagem extends Elemento {
Padrões de Projetos de Software com Java
Marcio Quirino - 50
public void apagar() {
// lógica para apagar a mensagem
}
public void criptografar() {
// lógica para criptografar a mensagem
}
}
public class Pasta extends Elemento {
private List filhos = new ArrayList();
public void adicionar(Elemento elem) {
filhos.add(elem);
}
public void remover(Elemento elem) {
filhos.remove(elem);
}
public void apagar() {
for (Elemento elemento : filhos) // apaga todos os filhos
elemento.apagar();
// lógica adicional para apagar a pasta
}
public void criptografar() {
for (Elemento elemento : filhos) // criptografa todos os filhos
elemento.criptografar();
// lógica adicional para criptografar a pasta
}
public class ServicoMensagem {
public void apagar(Elemento elemento) {
// código adicional de tratamento da requisição ficaria aqui
elemento.apagar(); // apaga o elemento: pode ser uma mensagem ou uma pasta
}
public void criptografar(Elemento elemento) {
// código adicional de tratamento da requisição ficaria aqui
elemento.criptografar(); // criptografa o elemento: pode ser uma mensagem ou
uma pasta
}
}
1. A classe Elemento corresponde ao participante Component do padrão, definindo as operações
para adicionar e remover elementos de um agregado, além das operações abstratas apagar e
criptografar, que se aplicam tanto a uma mensagem como a uma pasta de mensagens.
2. A classe Mensagem corresponde ao participante Leaf do padrão, definindo a implementação
específica das operações para apagar e criptografar uma mensagem.
3. A classe Pasta corresponde ao participante Composite do padrão, definindo a implementação
das operações de adição e remoção de elementos à pasta, além do comportamento específico
para as operações apagar e criptografar.
4. Como é comum na implementação do padrão Composite, as operações apagar e criptografar do
agregado chamam as operações de mesmo nome dos filhos, podendo adicionar algum
tratamento específico, além daquele implementado nas folhas.
5. ServicoMensagem representa um módulo cliente, que, simplesmente, chama a operação sem
precisar saber se o elemento é uma Mensagem ou uma Pasta.
Consequências e padrões relacionados ao Composite
O padrão Composite representa uma estrutura hierárquica de objetos, permitindo que os módulos
clientes possam manipular seus elementos de maneira uniforme por meio da mesma interface.
Em geral, os módulos clientes não precisam saber se estão lidando com um elemento primitivo (folha)
ou com um agregado, o que simplifica o código, pois não é necessário utilizar estruturas condicionais para
identificar a operação a ser chamada.
O padrão permite adicionar novos tipos de componentes sem que seja necessário alterar o código
dos módulos clientes.
Padrões de Projetos de Software com Java
Marcio Quirino - 51
Por outro lado, a estrutura é muito genérica, o que dificulta a implementação de hierarquias que
tenham regras de composição mais restritivas em relação aos tipos de elemento que um determinado
agregado possa ter.
Comentário
O padrão Iterator é, muitas vezes, utilizado para fazer o percurso pelos elementos de uma hierarquia
implementada com o padrão Composite.
O padrão Decorator também é utilizado com frequência junto com o padrão Composite. Nesse caso,
eles, tipicamente, herdam da mesma superclasse, fazendo com que as classes que representam o
participante Decorator tenham que implementar a interface definida pelo participante Component.
Intenção do padrão Facade
O padrão Facade tem o propósito de oferecer uma interface de alto nível para um componente ou
subsistema, de modo que os módulos clientes possam utilizá-lo mais facilmente, não precisando manipular
uma estrutura complexa de navegação de objetos, nem ficando vulnerável a alterações nesta estrutura.
Problema resolvido pelo padrão Facade
Exemplo
Imagine que você esteja implementando um sistema de comércio eletrônico que pode ser utilizado tanto em
uma aplicação web, como em um aplicativo para dispositivo móvel.
Uma estrutura comum para esse tipo de sistema é estratificar os componentes em camadas,
separando em camadas distintas os elementos que compõema interface com o usuário, a lógica do negócio
e os demais elementos dependentes de tecnologia, tais como armazenamento e recuperação de dados,
integração com sistemas e dispositivos externos, entre outros.
Suponha que para implementar uma parte da página de compras, o código precise manipular um
objeto CarrinhoRepository para recuperar o carrinho do cliente, um objeto Carrinho para adicionar produtos
ao carrinho do cliente, um objeto ProdutoRepository para recuperar produtos a serem apresentados ao
cliente, um objeto ValidadorCarrinho para verificar se o carrinho atende às regras do negócio, um objeto
para calcular o valor do frete até o destino definido pelo cliente e um objeto para calcular o prazo de entrega.
A estrutura de dependências resultante dessa solução é apresentada no diagrama de classes a
seguir.
Padrões de Projetos de Software com Java
Marcio Quirino - 52
O problema dessa estrutura é que o elemento PaginaCompras, que pertence à camada de interface
com usuário, precisa conhecer e manipular diversos elementos da camada de Lógica de Negócio, o que
torna seu código mais complexo e suscetível a modificações que ocorram na estrutura de objetos de
negócio.
Comentário
De forma geral, o problema resolvido pelo padrão Facade é permitir que um módulo cliente utilize um
subsistema ou uma camada complexa de um sistema sem precisar conhecer ou manipular diversos elementos e, com
isso, reduzir o acoplamento entre o subsistema e os módulos clientes.
Solução do padrão Facade
A estrutura da solução proposta pelo padrão Facade é apresentada no diagrama UML a seguir.
Consiste, basicamente, em definir uma classe correspondente ao participante Facade, que oferece
uma interface de alto nível ao participante Client, concentrando toda a manipulação dos elementos
pertencentes à camada ou subsistema. Portanto, o elemento Facade passa a ser o ponto focal de contato
entre o cliente e o subsistema, ou camada com o qual ele precisa se comunicar.
A aplicação do padrão no problema mencionado anteriormente nos leva à estrutura definida no
diagrama a seguir. O elemento PaginaCompras agora interage com uma interface de mais alto nível
oferecida pelo elemento ServicoCompras. Desse modo, toda a manipulação de objetos que estava presente
em PaginaCompras passa a ser implementada em ServicoCompras.
Padrões de Projetos de Software com Java
Marcio Quirino - 53
Consequências e padrões relacionados ao Facade
• O padrão Facade reduz a complexidade de implementação dos módulos clientes de um
subsistema, promovendo menor acoplamento entre o subsistema e os seus clientes.
• Isso permite que os componentes internos do subsistema possam ser modificados sem afetar os
módulos clientes.
• Entretanto, o padrão não impede que os módulos clientes possam acessar elementos do
subsistema, se necessário. Normalmente, o acesso a esses elementos é promovido e controlado
pelo módulo Facade.
• É possível reduzir ainda mais o acoplamento entre um subsistema e os seus clientes ao
implementar o Facade por meio de uma interface abstrata que pode ser implementada por
diferentes classes concretas.
Comentário
O padrão Abstract Factory ou um framework de injeção de dependências podem ser utilizados para criar a
implementação concreta de um subsistema, caso o Facade seja uma interface abstrata.
O padrão Mediator possui uma estrutura similar ao Facade, contudo, tem o propósito de abstrair a comunicação
entre objetos da mesma camada, centralizando funcionalidades que não sejam específicas de nenhum desses objetos.
O padrão Facade, por outro lado, tem o propósito de fornecer uma interface de alto nível para clientes de outros
subsistemas.
4. Padrões de projeto Flyweight e Proxy
Intenção do padrão Flyweight
O propósito do padrão Flyweight é permitir a utilização de diversos pequenos objetos de forma
eficiente, por meio de uma solução baseada em compartilhamento de objetos.
Problema resolvido pelo padrão Flyweight
Suponha que você esteja implementando uma aplicação envolvendo substâncias químicas.
Uma substância química simples é formada por átomos de um único elemento químico, como, por
exemplo, o hidrogênio (H2) e o enxofre (S8). Uma substância composta é formada por átomos de elementos
químicos diferentes como, por exemplo, a água (H2O) e o bicarbonato de sódio (NaHCO3).
Uma maneira de representar esses elementos em Java é expressa no código a seguir:
public class ElementoQuimico {
private String simbolo;
private String nome;
public ElementoQuimico(String simbolo, String nome) {
this.simbolo = simbolo;
this.nome = nome;
}
// getters e setters omitidos
}
public abstract class Substancia {
private String nome;
public Substancia(String nome) {
this.nome = nome;
}
// getters e setters omitidos
}
public class SubstanciaSimples extends Substancia {
private int atomos;
private ElementoQuimico elemento;
Padrões de Projetos de Software com Java
Marcio Quirino - 54
public SubstanciaSimples(String nome, ElementoQuimico elemento, int atomos) {
super(nome);
this.atomos = atomos;
this.elemento = elemento;
}
// getters e setters omitidos
}
public class SubstanciaComposta extends Substancia {
// Conjunto de elementos químicos e respectivas quantidades de átomos
private Map composicao;
public SubstanciaComposta(String nome, Map composicao) {
super(nome);
this.composicao = composicao;
}
// getters e setters omitidos
}
Um elemento químico contém os atributos símbolo e nome.
• A classe SubstanciaSimples tem o nome herdado da superclasse e define o número de
átomos do respectivo elemento químico.
• A classe SubstanciaComposta define a composição de elementos químicos e suas
respectivas quantidades de átomos em uma estrutura do tipo chave-valor (Map).
O código a seguir mostra como objetos dessas classes poderiam ser instanciados. Note como o
elemento químico oxigênio é instanciado repetidamente nas diferentes substâncias criadas.
public class Exemplo {
public static void main(String[] args) {
SubstanciaSimples s1 = new SubstanciaSimples("Oxigênio",
new ElementoQuimico("O", "Oxigênio"), 2);
SubstanciaSimples s2 = new SubstanciaSimples("Ozônio",
new ElementoQuimico("O", "Oxigênio"), 3);
Map composicaoAgua = new HashMap();
composicaoAgua.put(new ElementoQuimico("H", "Hidrogênio"), 2);
composicaoAgua.put(new ElementoQuimico("O", "Oxigênio"), 1);
SubstanciaComposta s3 = new SubstanciaComposta("Agua", composicaoAgua);
}
}
A definição de elemento químico é um exemplo característico do que denominamos objeto imutável,
isto é, os valores de seus atributos não mudam.
Comentário
Considerando que um elemento químico é imutável, e supondo que precisamos criar milhares de substâncias
na nossa aplicação de química, a solução apresentada é altamente ineficiente no uso de memória, pois serão
instanciados milhares de pequenos objetos correspondentes aos elementos químicos.
O problema que esse padrão tenta resolver, portanto, consiste em como podemos compartilhar
objetos imutáveis, ou as partes imutáveis de objetos, de modo a utilizar os recursos de memória de forma
mais eficiente.
Solução do padrão Flyweight
A estrutura da solução proposta pelo padrão Flyweight está representada no diagrama de classes a
seguir.
Padrões de Projetos de Software com Java
Marcio Quirino - 55
A ideia central consiste na construção de um pool de objetos compartilhados, criado e atualizado por
uma fábrica. Desse modo, os objetos clientes, em vez de instanciarem diretamente esses objetos, passam
a fazer uso da fábrica.
O participante FlyweightFactory representa a fábrica que gerencia o pool de objetos compartilháveis.
Sempre que um cliente pedir um objeto flyweight, a fábrica retorna uma já existente ou cria uma nova,se
ainda não existir uma.
Os objetos flyweight estão organizados em uma hierarquia com duas subclasses:
• ConcreteFlyweight, representando os objetos que são compartilhados.
• UnsharedConcreteFlyweight, que representa os objetos que não são compartilhados.
Isso acontece porque o padrão não obriga que todos os objetos que implementam a interface abstrata
Flyweight sejam compartilhados, mas permite que os clientes fiquem isolados da decisão de compartilhar
ou não os objetos de uma determinada classe.
Portanto, podemos iniciar um projeto definindo que objetos de uma classe não serão compartilhados
(assumindo o papel de UnsharedConcreteFlyweight) e depois passarmos para uma solução em que eles
sejam compartilhados, assumindo o papel de ConcreteFlyweight, sem qualquer impacto para os módulos
clientes.
Comentário
Note que um ConcreteFlyweight somente compartilha o seu estado intrínseco, isto é, um conjunto de
propriedades imutáveis. Se os objetos compartilhados de uma classe possuírem propriedades extrínsecas, ou seja,
que possam mudar de valor, elas deverão ser mantidas pelos módulos clientes, que poderão passar esses dados como
parâmetros de operações do flyweight.
Padrões de Projetos de Software com Java
Marcio Quirino - 56
O exemplo a seguir mostra como o código das substâncias químicas pode ser escrito com a utilização
do padrão Flyweight. Os elementos químicos, em vez de serem instanciados diretamente pelos módulos
clientes, passam a ser instanciados pela classe ElementoQuimicoFactory, que desempenha o papel do
participante FlyweightFactory do padrão. Ela armazena todos os elementos já criados e se necessário, cria
novos elementos. O programa exemplo cria as substâncias solicitando os elementos para a fábrica. Desse
modo, apenas dois elementos são instanciados (H e O), em vez de quatro, como no programa original, dado
que o objeto representando o elemento oxigênio é compartilhado por todos os objetos que representam as
substâncias instanciadas.
Note que o construtor da classe ElementoQuimico não é mais público, para evitar sua instanciação
direta. As classes ElementoQuimico e ElementoQuimicoFactory devem ficar no mesmo pacote para permitir
que a fábrica tenha acesso ao construtor de ElementoQuimico.
public class ElementoQuimicoFactory {
private Map elementos;
public ElementoQuimico criarElemento(String simbolo, String nome) {
ElementoQuimico elemento = elementos.get(simbolo);
if (elemento == null) {
elemento = new ElementoQuimico(simbolo, nome);
elementos.put(simbolo, elemento);
}
return elemento;
}
}
public class ElementoQuimico {
private String simbolo;
private String nome;
ElementoQuimico(String simbolo, String nome) {
this.simbolo = simbolo;
this.nome = nome;
}
// getters e setters omitidos
}
public class Exemplo {
public static void main(String[] args) {
ElementoQuimicoFactory factory = new ElementoQuimicoFactory();
SubstanciaSimples s1 = new SubstanciaSimples("Oxigênio",
factory.criarElemento("O", "Oxigênio"), 2);
SubstanciaSimples s2 = new SubstanciaSimples("Ozônio",
factory.criarElemento("O", "Oxigênio"), 3);
Map composicaoAgua = new HashMap();
composicaoAgua.put(factory.criarElemento("H", "Hidrogenio"), 2);
composicaoAgua.put(factory.criarElemento("O", "Oxigênio"), 1);
SubstanciaComposta s3 = new SubstanciaComposta("Agua", composicaoAgua);
}
}
Consequências e padrões relacionados ao Flyweight
O padrão Flyweight permite a utilização mais racional da memória em casos nos quais exista a
criação de um número elevado de pequenos objetos replicados. Ele é aplicável, especialmente, quando
esses objetos possuírem um estado intrínseco imutável.
Um exemplo conhecido de objetos imutáveis em Java corresponde aos objetos da classe String.
As constantes literais do tipo String em Java são implementadas como objetos String imutáveis,
compartilhados em um pool de constantes. Desse modo, apesar de uma constante literal String ser utilizada
em vários módulos de um programa, apenas uma instância dessa constante é criada.
Padrões de Projetos de Software com Java
Marcio Quirino - 57
Comentário
O padrão Flyweight pode ser combinado com o padrão Composite na implementação de estruturas de árvores
com folhas compartilhadas. Outro padrão frequentemente combinado com o Flyweight é o State.
No padrão State, cada estado de um objeto é representado como uma classe. Desse modo, se uma
classe Pedido, por exemplo, tiver cinco estados possíveis e instanciarmos mil pedidos, teremos para cada
pedido um objeto estado associado. Portanto, se essa estrutura for implementada, instanciando-se
diretamente cada objeto, teremos dois mil objetos em memória. Entretanto, se o padrão Flyweight for
utilizado, teremos apenas mil objetos da classe Pedido mais cinco objetos (um para cada estado possível)
compartilhados por esses mil objetos.
Conseguiu perceber como o uso da memória fica mais racional nesse caso?
Intenção do padrão Proxy
O propósito do padrão Proxy é fornecer aos clientes um objeto proxy, com a mesma interface do
objeto destino real, que delega as requisições dos clientes para o objeto real.
Esse padrão é muito utilizado quando o objeto real é remoto ou de instanciação muito custosa, como,
por exemplo, o caso de imagens, ou quando se deseja interceptar a requisição antes que ela chegue ao
objeto real para fins de autorização, monitoração, validação, entre outros.
Problema resolvido pelo padrão Proxy
Quando estamos desenvolvendo um programa orientado a objetos, implementar a chamada de uma
operação de outro objeto é uma tarefa simples caso eles estejam no mesmo processo. Imagine, agora, que
esses objetos estejam em processos diversos, rodando em máquinas diferentes, como é comum em projetos
distribuídos.
Para que um objeto consiga chamar uma operação de um objeto remoto, é necessário estabelecer
uma conexão, empacotar os dados para transmissão e fazer a chamada, ou seja, não é simplesmente
chamar uma operação como ocorre em um programa orientado a objetos simples.
Comentário
Para você entender melhor o problema, veja o código a seguir, que implementa a interação de um módulo
cliente com um módulo EJB servidor que executa uma transferência entre contas. Imagine que o módulo cliente seja
uma parte da implementação da camada de interface com o usuário que precisa solicitar esse serviço ao componente
de negócio remoto. Se você não conhecer a tecnologia EJB, não tem problema.
O importante é você perceber a complexidade que foi inserida na implementação para que o módulo
cliente conseguisse fazer a chamada da operação do componente EJB correspondente à lógica de negócio.
public class ModuloCliente {
private ServicoConta session; // referência para o objeto remoto
private static final Class homeClazz = ServicoContaHome.class;
public void executarTransferencia(Transferencia transf) throws ServicoContaException
{
try {
ServicoContaHome home = (ServicoContaHome) getHome("ServicoConta",
homeClazz);
session = home.create();
session.executarTransferencia(transf); // chamada delegada ao objeto
real
} catch (Exception ex) {
throw new ServicoContaException(ex);
}
}
public EJBHome getHome(String name, Class clazz) throws NamingException {
Object objref = context.lookup(name);
Padrões de Projetos de Software com Java
Marcio Quirino - 58
EJBHome home = (EJBHome) PortableRemoteObject.narrow(objref, clazz);
return home;
}
}
Imagine, agora, que o sistema que você está desenvolvendo tenha dezenas de módulos clientes,
que precisam interagir com os objetos EJB que realizam a lógica de negócio. Indo além, suponha que você
precise mudar a tecnologia de invocação dos serviços de EJB para uma REST API.
Qual seria o impacto nos módulos clientes?
Se pensou em um impacto gigantesco, você acertou!
Embora oComposite ................................................................................................................ 48
Problema resolvido pelo padrão Composite ......................................................................................... 48
Solução do padrão Composite ............................................................................................................. 49
Consequências e padrões relacionados ao Composite ....................................................................... 50
Intenção do padrão Facade ..................................................................................................................... 51
Problema resolvido pelo padrão Facade .............................................................................................. 51
Solução do padrão Facade .................................................................................................................. 52
Consequências e padrões relacionados ao Facade ............................................................................. 53
4. Padrões de projeto Flyweight e Proxy ................................................................................... 53
Intenção do padrão Flyweight .................................................................................................................. 53
Solução do padrão Flyweight ............................................................................................................... 54
Consequências e padrões relacionados ao Flyweight ......................................................................... 56
Intenção do padrão Proxy ........................................................................................................................ 57
Problema resolvido pelo padrão Proxy ................................................................................................. 57
Solução do padrão Proxy ......................................................................................................................... 58
Consequências e padrões relacionados ao Proxy ............................................................................... 60
5. Conclusão............................................................................................................................... 60
Considerações Finais ............................................................................................................................... 60
Referências .............................................................................................................................................. 61
Explore+ ................................................................................................................................................... 61
Padrões GoF Comportamentais .......................................................................................................... 62
Apresentação ........................................................................................................................................... 62
Propósito .................................................................................................................................................. 62
Preparação ............................................................................................................................................... 62
Padrões de Projetos de Software com Java
Marcio Quirino - 3
1. Padrões de projeto comportamentais Chain of Responsibility, Command e Iterator ........... 62
Padrão Chain of Responsibility ................................................................................................................ 62
Intenção do padrão Chain of Responsibility ......................................................................................... 62
Problema resolvido pelo padrão Chain of Responsibility ..................................................................... 62
Solução do padrão Chain of Responsibility .......................................................................................... 63
Consequências e padrões relacionados ao padrão Chain of Responsibility ........................................ 66
Padrão Command .................................................................................................................................... 67
Intenção do padrão Command ............................................................................................................. 67
Problema resolvido pelo padrão Command ......................................................................................... 67
Solução do padrão Command .............................................................................................................. 68
Consequências e padrões relacionados ao padrão Command ............................................................ 70
Padrão Iterator ......................................................................................................................................... 70
Intenção do padrão Iterator .................................................................................................................. 70
Problema resolvido pelo padrão Iterator............................................................................................... 70
Solução do padrão Iterator ................................................................................................................... 71
Consequências e padrões relacionados ao padrão Iterator ................................................................. 72
2. Padrões de projeto comportamentais Mediator, Memento e Strategy ................................. 73
Padrão Mediator ....................................................................................................................................... 73
Intenção do padrão Mediator ................................................................................................................ 73
Problema resolvido pelo padrão Mediator ............................................................................................ 73
Solução do padrão Mediator ................................................................................................................ 73
Consequências e padrões relacionados ao padrão Mediator ............................................................... 75
Padrão Memento ...................................................................................................................................... 76
Intenção do padrão Memento ............................................................................................................... 76
Problema resolvido pelo padrão Memento ........................................................................................... 76
Solução do padrão Memento ............................................................................................................... 77
Consequências e padrões relacionados ao padrão Memento .............................................................. 78
Padrão Strategy ....................................................................................................................................... 79
Intenção do padrão Strategy ................................................................................................................ 79
Problema resolvido pelo padrão Strategy............................................................................................. 79
Solução do padrão Strategy ................................................................................................................. 80
Consequências e padrões relacionados ao padrão Strategy ............................................................... 81
3. Padrões de projeto comportamentais Observer, Visitor e State ...........................................próprio EJB já contenha uma implementação do padrão Proxy, oferecendo um objeto proxy
local que abstrai os detalhes de transporte e comunicação via rede com o objeto EJB remoto, o exemplo
anterior mostra que os módulos clientes continuam tendo que lidar com detalhes da tecnologia envolvida na
comunicação entre as camadas de uma aplicação.
Solução do padrão Proxy
A estrutura da solução proposta pelo padrão Proxy está representada no diagrama de classes a
seguir.
A dinâmica entre os participantes é ilustrada no diagrama de sequência a seguir. Note que o papel
do proxy é fundamentalmente repassar a chamada ao objeto destino.
O principal participante do padrão é o elemento Proxy que controla o acesso ao objeto destino.
Existem diferentes tipos de proxy:
Padrões de Projetos de Software com Java
Marcio Quirino - 59
A. Remote proxy
✓ É responsável por oferecer uma interface local idêntica à implementada pelo componente
remoto de destino (RealSubject), isolando o cliente das questões envolvendo
estabelecimento de conexão, codificação do conteúdo da requisição e tratamento de erros.
Esse proxy é utilizado na comunicação entre objetos distribuídos em diferentes máquinas.
B. Virtual proxy
✓ É utilizado quando o objeto real tem uma carga muito custosa ou demorada. Uma interface
com o usuário com imagens pesadas, por exemplo, pode fazer uso de virtual proxies para o
conteúdo real das imagens, permitindo apresentar mensagens ou imagens mais leves e,
somente sob o comando do usuário, trazer a imagem completa para a tela. Outro uso para
esse tipo de proxy ocorre em frameworks de persistência de objetos, quando um objeto está
relacionado a centenas de outros e desejamos acessar apenas as propriedades básicas do
objeto, sem a necessidade de carregar todos os objetos relacionados, estratégia conhecida
como lazy instantiation.
C. Interceptor proxy
✓ É utilizado quando a chamada para o objeto destino deve ser interceptada para realização de
atividades como verificação de permissão de acesso, monitoração, redirecionamento da
chamada, entre outras possibilidades, para, então, ser redirecionada ao objeto destino.
O código a seguir mostra a implementação de um proxy para o componente de negócio
implementado em EJB. Esse é um padrão J2EE conhecido como BusinessDelegate, implementado
utilizando o padrão de projeto proxy.
A classe ServicoContaProxyEJB implementa uma interface genérica (ServicoContaDelegate) que
replica as operações oferecidas pelo objeto remoto destino. A implementação desse proxy encapsula o
mecanismo de conexão com o componente de negócio, neste caso, EJB. Se implementarmos um
mecanismo de acesso via REST API, bastaria criarmos outro proxy implementando a interface
ServicoContaDelegate, delegando as chamadas para essa API. Perceba que a operação
executarTransferencia delega a execução para o componente remoto, mas, para isso, o proxy precisa se
conectar ao objeto EJB destino.
public interface ServicoContaDelegate {
void executarTransferencia(Transferencia tranf) throws ServicoContaException;
}
public class ServicoContaProxyEJB implements ServicoContaDelegate {
private ServicoConta session; // referência para o objeto remoto
private static final Class homeClazz = ServicoContaHome.class;
public void executarTransferencia(Transferencia transf) throws ServicoContaException
{
try {
session = getSession(homeClazz); // conexão com o objeto remoto
session.executarTransferencia(transf); // chamada delegada ao objeto
real
} catch (Exception ex) {
throw new ServicoContaException(ex);
}
}
public ServicoConta getSession(Class homeClazz) throws ServicoContaException {
try {
ServicoContaHome home = (ServicoContaHome) getHome("ServicoConta",
homeClazz);
session = home.create();
} catch (Exception ex) {
throw new ServicoContaException(ex);
}
}
public EJBHome getHome(String name, Class clazz) throws NamingException {
Padrões de Projetos de Software com Java
Marcio Quirino - 60
Object objref = context.lookup(name);
EJBHome home = (EJBHome) PortableRemoteObject.narrow(objref, clazz);
return home;
}
}
O código a seguir mostra como ficariam os módulos clientes que precisassem requisitar um serviço
de componente de negócio. Note que a implementação desses clientes está totalmente isolada dos detalhes
de conexão e tratamento de exceções inerentes ao protocolo de comunicação com o objeto remoto.
public class ModuloCliente {
private ServicoContaDelegate servicoConta;
public ModuloCliente(ServicoContaDelegate servicoConta) {
this.servicoConta = servicoConta;
}
public void executarTransferencia(Transferencia transf) {
try {
servicoConta.executarTransferencia(transf);
} catch (ServicoContaException ex) {
// apresenta mensagem de erro
}
}
}
Consequências e padrões relacionados ao Proxy
O padrão Proxy introduz um nível de indireção para o acesso a um objeto. Esse nível adicional
possibilita a transparência de localização do objeto destino para os objetos clientes, caso o objeto destino
esteja em outra máquina. Além disso, é possível construir objetos grandes ou numerosos sob demanda sem
mudar a interface oferecida ao módulo cliente.
Comentário
O padrão Proxy possui algumas similaridades com o padrão Adapter. Ambos oferecem uma forma de acesso
indireta ao objeto destino, porém, o padrão Adapter traduz uma interface padrão em uma interface específica oferecida,
tipicamente, por um componente de terceiros, enquanto o padrão proxy preserva a mesma interface oferecida pelo
objeto alvo.
O padrão Decorator possui uma implementação similar a um proxy, porém, tem um propósito diferente, por
adicionar novas funcionalidades a um objeto, enquanto um proxy mantém as funcionalidades oferecidas pelo objeto
destino.
5. Conclusão
Considerações Finais
Neste conteúdo, vimos como os padrões de projeto GoF estruturais podem auxiliar na criação de
soluções de projeto mais flexíveis e menos acopladas.
O padrão Adapter é muito utilizado para isolar módulos clientes das diferentes implementações de
um mesmo serviço oferecidas por terceiros. O desafio fundamental do padrão Adapter reside na definição
de uma interface comum às diversas soluções.
Os padrões Bridge e Adapter possuem uma estrutura de solução similar, porém, com objetivos
distintos. Enquanto o padrão Adapter visa adaptar uma interface comum a diferentes implementações,
tipicamente conhecidas a posteriori, o padrão Bridge promove uma separação a priori entre uma abstração
e as possíveis implementações da sua representação interna, permitindo que ambas possam evoluir de
forma independente.
O padrão Decorator é uma forma mais flexível de adicionar novos comportamentos a classes já
existentes, se comparada às soluções baseadas em herança. A sua estrutura de solução é baseada na
Padrões de Projetos de Software com Java
Marcio Quirino - 61
composição de objetos que compartilham uma interface genérica comum, gerando estruturas similares a
uma cebola ou a uma matrioska (você conhece aquela série de bonecas russas de tamanhos variados
colocadas umas dentro das outras?).
O padrão Composite oferece uma estrutura de composição recursiva que permite o tratamento de
hierarquias de objetos que possuam operações que se apliquem tanto às folhas como aos agregados. O
padrão Facade é muito utilizado na estruturação da arquitetura de sistemas, oferecendo uma interface de
alto nível para cada subsistema ou componente macro da solução. O padrão Flyweight oferece uma forma
de utilização de memória mais eficiente em casos nos quais precisamos instanciar uma grande quantidade
de objetos imutáveis ou que possuam uma parte significativa do seu estado imutável.
O padrão Proxy fornece um nível de indireção a um objeto, de forma parecida com o padrão
Decorator, pois ambos possuem uma referência para um objeto para o qual as requisições sãodirecionadas.
Entretanto, esses padrões possuem objetivos distintos. O Decorator se preocupa em adicionar
funcionalidades ao objeto alvo, enquanto o Proxy oferece a mesma interface do objeto destino, porém,
isolando o cliente de problemas como acesso remoto ou otimização de instanciação de objetos com
utilização intensiva de memória.
Referências
GAMMA, E.; HELM, R.; JOHNSON, R.; VLISSIDES, J. Design patterns: elements of reusable object-
oriented software. 1. ed. Boston: Addison-Wesley, 1994.
METSKER, S. J.; WAKE, W. C. Design patterns in Java. 1.ed. Boston: Addison-Wesley, 2006.
Explore+
Para saber mais sobre a programação orientada a objetos, acesse o site da DevMedia e leia o artigo
Utilização dos princípios SOLID na aplicação de padrões de projeto.
O site Padrões de Projeto/Design patterns – Refactoring.Guru apresenta um conteúdo interativo e
bastante completo de todos os padrões GoF, com exemplos de código em diversas linguagens de
programação.
Além dos padrões GoF tradicionais, outros voltados para o desenvolvimento de aplicações
corporativas em Java EE podem ser encontrados no livro Java EE 8 Design Patterns and Best Practices,
escrito por Rhuan Rocha e João Purificação. A obra aborda padrões para interface com o usuário, lógica do
negócio, integração de sistemas, orientação a aspectos, programação reativa e microsserviços.
Padrões de Projetos de Software com Java
Marcio Quirino - 62
Padrões GoF Comportamentais
Apresentação
O grupo de padrões GoF comportamentais engloba Chain of Responsibility, Command, Interpreter,
Iterator, Mediator, Memento, Observer, State, Strategy, Template Method e Visitor. São padrões ligados a
algoritmos e à atribuição de responsabilidades em projetos orientados a objetos. Buscaremos identificar
oportunidades para aplicá-los. Precisamos compreender essa família de padrões e saber como utilizá-los,
pois, caso contrário, nossas soluções podem se tornar pouco flexíveis e de difícil manutenção.
Propósito
Compreender os padrões ligados a algoritmos e à atribuição de responsabilidades em projetos
orientados a objetos, bem como identificar oportunidades para a sua aplicação são habilidades importantes
para um projetista de software, pois, sem elas, as soluções geradas podem ser pouco flexíveis e dificultar a
evolução de sistemas de software em prazo e custo aceitáveis.
Preparação
Antes de iniciar o conteúdo, é recomendado instalar em seu computador um programa que lhe
permita elaborar modelos sob a forma de diagramas da UML (Linguagem Unificada de Modelagem). Nossa
sugestão inicial é o Free Student License for Astah UML, usado nos exemplos deste estudo. Os arquivos
Astah com diagramas UML utilizados neste conteúdo estão disponíveis para download.
Recomendamos, também, a instalação de um ambiente de programação em Java, como o Apache
Netbeans. Porém, antes de instalar o Netbeans, é necessário ter instalado o JDK (Java Development Kit)
referente à edição Java SE (Standard Edition), que pode ser encontrado no site da Oracle Technology
Network: Java SE - Downloads | Oracle Technology Network | Oracle.
1. Padrões de projeto comportamentais Chain of
Responsibility, Command e Iterator
Padrão Chain of Responsibility
Não são raras as situações em que uma requisição precisa passar por diferentes tratamentos,
obedecendo a uma sequência específica, como em uma linha de produção. Por exemplo, ao tratar uma
requisição no protocolo HTTP, pode ser necessário decriptar, descompactar e definir a codificação, antes
que a informação possa ser tratada. E uma boa estratégia seria definir uma sequência de agentes para as
transformações, cada um recebendo a requisição, executando sua parte, e repassando para o próximo
agente. Esse é o modelo adotado no Chain of Responsibility, e aqui veremos como ele pode ser útil em
nossos projetos.
Intenção do padrão Chain of Responsibility
Chain of Responsibility é um padrão que permite o envio de uma requisição para o primeiro elemento
de uma cadeia de objetos que podem realizar algum processamento relacionado a essa requisição, fazendo
com que o objeto requisitante não precise ficar acoplado a todos os objetos da cadeia, mas apenas ao objeto
para o qual ele envia a requisição.
Problema resolvido pelo padrão Chain of Responsibility
Imagine que você esteja implementando um módulo tarifador de ligações telefônicas, responsável
por calcular o custo de cada ligação telefônica efetuada em uma central telefônica do tipo PABX de uma
empresa.
Padrões de Projetos de Software com Java
Marcio Quirino - 63
Suponha que exista uma regra de precificação específica para cada tipo de ligação: ligação interna
entre ramais, ligação local, ligação DDD e ligação DDI. Uma solução possível para esse problema seria
concentrar toda a lógica de tarifação em uma única classe, conforme podemos ver no código a seguir.
public class ServicoTarifacaoTelefonica {
// método que calcula o custo de uma ligação recebida como parâmetro
public BigDecimal tarifar(Ligacao ligacao) {
// de acordo com o tipo da ligação, chama o método apropriado de cálculo
if (ligacao.isInterna()) {
return tarifarLigacaoInterna (ligacao);
} else if (ligacao.isLocal()) {
return tarifarLigacaoLocal (ligacao);
} else if (ligacao.isDDD()) {
return tarifarLigacaoDDD (ligacao);
} else if (ligacao.isDDI()) {
return tarifarLigacaoDDI (ligacao);
} else {
return new BigDecimal(0);
}
}
private BigDecimal tarifarLigacaoInterna (Ligacao ligacao) {
// código para tarifação de ligação interna
}
private BigDecimal tarifarLigacaoLocal (Ligacao ligacao) {
// código para tarifação de ligação local
}
private BigDecimal tarifarLigacaoDDD (Ligacao ligacao) {
// código para tarifação de ligação DDD
}
private BigDecimal tarifarLigacaoDDI (Ligacao ligacao) {
// código para tarifação de ligação DDI
}
}
Essa solução concentra toda a lógica de tarifação em um único módulo, tornando-o extenso e
complexo. Imagine como ficaria esse módulo, caso outros tipos de ligação fossem adicionados, como, por
exemplo, ligações para celular ou ligações considerando as operadoras de origem e destino.
Solução do padrão Chain of Responsibility
Suponha que você esteja em uma grande loja de departamentos querendo comprar uma televisão.
O vendedor apresenta as condições para a compra, e você pede um desconto. Se o desconto estiver dentro
do limite do vendedor, ele mesmo poderá aprová-lo. Caso contrário, o vendedor pedirá para você aguardar,
pois ele terá que solicitar a autorização do supervisor. Se estiver no limite permitido para o supervisor, o
desconto será aprovado, caso contrário, este terá que levar o pedido até o gerente, que poderá aprová-lo
ou não. Ao final desse processo, o vendedor voltará para comunicar o resultado, ou seja, se o desconto foi
autorizado.
Perceba que você falou apenas com o vendedor, mas, a partir da sua requisição, uma cadeia de
responsabilidade de aprovação de desconto foi acionada até a decisão final ser tomada.
A solução proposta pelo padrão Chain of Responsibility é inspirada nessa abordagem da loja de
departamentos. Em vez de um módulo concentrar a responsabilidade de conhecer todos os objetos ou
métodos participantes da solução, o processamento é dividido em pequenos módulos que podem ser
combinados de diferentes maneiras. A ideia é construir uma cadeia de objetos, na qual cada objeto pode
fazer algum processamento com a requisição ou simplesmente repassá-la para o seu sucessor na cadeia.
O diagrama de classes a seguir apresenta a estrutura da solução proposta pelo padrão.
Padrões de Projetosde Software com Java
Marcio Quirino - 64
Diagrama de classes.
O participante Client representa um módulo cliente responsável pelo envio de uma requisição, que
será tratada por uma cadeia de objetos descendentes do participante abstrato Handler. Cada elemento da
cadeia, representada pelos participantes do tipo ConcreteHandler, pode tratar a requisição ou repassá-la
para o seu sucessor, definido pelo autorrelacionamento presente no participante Handler. Portanto, todo
objeto ConcreteHandler tem um sucessor, que é uma instância de uma classe descendente de Handler.
Os objetos ConcreteHandler formam a cadeia ilustrada no diagrama de sequência a seguir, em que
a chamada do objeto Client à operação HandleRequest do primeiro ConcreteHandler pode ser encaminhada
para o seu sucessor na cadeia, por meio de uma chamada à operação de mesmo nome.
Diagrama de sequência.
Você se lembra como funciona o tratamento de exceções em Java? As chamadas de métodos em
um instante de execução de um programa formam uma cadeia, também conhecida por pilha de execução.
Quando um bloco catch em um método captura uma exceção, ele pode:
A. Fazer um tratamento local da exceção e encerrar o fluxo de exceção.
B. Fazer um tratamento local da exceção e repassá-la para o método chamador ou bloco try-catch
mais externo.
C. Somente repassar a exceção para o método chamador ou bloco try-catch mais externo, sem fazer
nenhum tratamento local.
A cadeia de objetos que forma a estrutura de solução desse padrão funciona de forma similar ao
tratamento de exceções em Java, pois cada Handler pode fazer um processamento local da requisição e/ou
repassá-la para o seu sucessor.
Agora, vamos reestruturar a solução apresentada anteriormente para o problema da tarifação de
ligações, aplicando o padrão Chain of Responsibility. Primeiro, vamos definir a classe abstrata
Padrões de Projetos de Software com Java
Marcio Quirino - 65
TarifadorLigacao correspondente ao participante Handler do padrão. Ela possui um atributo correspondente
ao seu sucessor e à operação tarifar, que repassa a chamada para o objeto sucessor. Note que a operação
tarifar corresponde à operação HandleRequest definida na estrutura do padrão.
public abstract class TarifadorLigacao {
private TarifadorLigacao sucessor;
public TarifadorLigacao setSucessor(TarifadorLigacao sucessor) {
this.sucessor = sucessor;
return this; // retorna ele próprio para facilitar a construção da cadeia de
objetos
}
public BigDecimal tarifar (Ligacao ligacao) {
// este método na superclasse repassa a chamada para o próximo objeto da cadeia
return (sucessor != null)
? sucessor.tarifar (ligacao);
: new BigDecimal(0);
}
}
Em seguida, vamos implementar uma classe tarifador para cada tipo de ligação. Cada tarifador
específico corresponde ao participante ConcreteHandler definido no padrão. A implementação do método
tarifar em cada tarifador específico retorna o custo da ligação, se ela for do tipo apropriado ou, caso contrário,
chama o método tarifar definido na superclasse que, por sua vez, repassa a responsabilidade do cálculo
para o próximo tarifador da cadeia.
public class TarifadorLigacaoInterna extends TarifadorLigacao {
// método que calcula o custo de uma ligação recebida como parâmetro
public BigDecimal tarifar(Ligacao ligacao) {
if (ligaçao.isInterna()) {
return tarifarLigacaoInterna(ligacao); // se for ligação
interna, tarifa aqui
} else {
return super.tarifar(ligacao); // se não for, repassa
para próximo da cadeia
}
}
private BigDecimal tarifarLigacaoInterna(Ligacao ligacao) {
// código para tarifação de ligação interna
}
}
public class TarifadorLigacaoLocal extends TarifadorLigacao {
// método que calcula o custo de uma ligação recebida como parâmetro
public BigDecimal tarifar (Ligacao ligacao) {
if (ligaçao.isLocal()) {
return tarifarLigacaoLocal(ligacao); // se ligação for
local, tarifa aqui
} else {
return super.tarifar (ligacao); // se não for, passa para
o próximo da cadeia
}
}
private BigDecimal tarifarLigacaoLocal (Ligacao ligacao) {
// código para tarifação de ligação local
}
}
Em seguida, vamos implementar uma classe tarifador para cada tipo de ligação. Cada tarifador
específico corresponde ao participante ConcreteHandler definido no padrão. A implementação do método
tarifar em cada tarifador específico retorna o custo da ligação, se ela for do tipo apropriado ou, caso contrário,
chama o método tarifar definido na superclasse que, por sua vez, repassa a responsabilidade do cálculo
para o próximo tarifador da cadeia.
public class TarifadorLigacaoInterna extends TarifadorLigacao {
// método que calcula o custo de uma ligação recebida como parâmetro
public BigDecimal tarifar(Ligacao ligacao) {
Padrões de Projetos de Software com Java
Marcio Quirino - 66
if (ligaçao.isInterna()) {
return tarifarLigacaoInterna(ligacao); // se for ligação interna,
tarifa aqui
} else {
return super.tarifar(ligacao); // se não for, repassa para próximo da
cadeia
}
}
private BigDecimal tarifarLigacaoInterna(Ligacao ligacao) {
// código para tarifação de ligação interna
}
}
public class TarifadorLigacaoLocal extends TarifadorLigacao {
// método que calcula o custo de uma ligação recebida como parâmetro
public BigDecimal tarifar (Ligacao ligacao) {
if (ligaçao.isLocal()) {
return tarifarLigacaoLocal(ligacao); // se ligação for local, tarifa
aqui
} else {
return super.tarifar (ligacao); // se não for, passa para o próximo da
cadeia
}
}
private BigDecimal tarifarLigacaoLocal (Ligacao ligacao) {
// código para tarifação de ligação local
}
}
Finalmente, observe como na nova implementação a classe ServicoTarifacaoTelefonica pode operar
com qualquer tarifador que seja plugado no seu construtor. A operação tarifar dessa classe simplesmente
faz a requisição para o primeiro tarifador da cadeia.
A classe ConfiguradorServicoTarifacaoTelefonica é um exemplo simples de como uma cadeia de
responsabilidades pode ser criada e injetada em uma classe cliente. Ela cria uma cadeia de tarifadores, um
para cada tipo de ligação, usando a operação setSucessor para ligar um tarifador ao outro. Uma vez criada,
a cadeia é passada como argumento para o construtor da classe ServicoTarifacaoTefefonica.
public class ServicoTarifacaoTelefonica() {
private TarifadorLigacao tarifador;
public ServicoTarifacaoTelefonica (TarifadorLigacao tarifador) {
this.tarifador = tarifador;
}
public BigDecimal tarifar (LigacaoTelefonica ligacao) {
return tarifador.tarifar(ligacao); // chama o primeiro tarifador da cadeia
}
}
public class ConfiguradorServicoTarifacaoTelefonica {
public ServicoTarifacaoTelefonica getInstance() {
// cria a cadeia de tarifadores
TarifadorLigacao cadeiaTarifadores = new TarifadorLigacaoInterna().
setSucessor(new TarifadorLigacaoLocal()).
setSucessor(new TarifadorLigacaoDDD()).
setSucessor(new TarifadorLigacaoDDI());
// instancia o serviço passando a cadeia de tarifadores
return new ServicoTarifacaoTelefonica(cadeiaTarifadores);}
}
Consequências e padrões relacionados ao padrão Chain of Responsibility
O padrão Chain of Responsibility reduz a complexidade de uma classe que tenha que lidar com várias
possibilidades de tratamento de uma requisição, transformando as diversas operações e estruturas
condicionais complexas originalmente existentes em um conjunto de objetos interconectados, que podem
ser combinados de diferentes formas, gerando uma solução menos acoplada e mais flexível. Por outro lado,
Padrões de Projetos de Software com Java
Marcio Quirino - 67
existe o risco de uma requisição não ser respondida de forma adequada, caso a configuração da cadeia não
seja corretamente realizada.
Atenção
Esse padrão é frequentemente utilizado em conjunto com o padrão Composite. Nesse caso, não é necessário
implementar um sucessor, dado que podemos utilizar o relacionamento entre o agregado e as suas partes para
encadear as chamadas pelos elementos da estrutura de composição.
Padrão Command
Imagine que você precisa criar uma simples calculadora para números inteiros, com as operações
fundamentais, ou seja, soma, subtração, multiplicação e divisão inteira, algo que seria resolvido com uma
estrutura de seleção. Porém, gostaram muito da sua calculadora, e pediram novas operações como
exponenciação e módulo. Não seria mais interessante definir um modelo abstrato, com entrada de dois
inteiros, execução e retorno do resultado inteiro, e derivar classes concretas para cada operação? O padrão
Command visa esse tipo de modelagem, encapsulando os algoritmos em uma família de classes, o que
facilita muito a evolução de nossos sistemas.
Intenção do padrão Command
Command é um padrão que encapsula uma requisição em um objeto. Em projetos que não utilizam
esse padrão, uma requisição é normalmente realizada por meio de uma simples chamada de operação. O
encapsulamento de requisições em objetos desacopla o requisitante e o objeto executor, o que possibilita:
1. Parametrizar as requisições disparadas pelos clientes;
2. Criar filas de requisições;
3. Registrar o histórico de requisições;
4. Implementar operações para desfazer (undo) ou refazer (redo) o processamento realizado
para atender uma requisição.
Problema resolvido pelo padrão Command
Imagine que você esteja desenvolvendo um jogo no qual as ações associadas às teclas ou aos
botões do mouse possam ser configuradas. Veja no quadro a seguir um exemplo de configuração.
Evento Ação
Tecla W ir para frente
Tecla S Ir para trás
Tecla espaço Pular
Botão 4 do mouse Pular
Quadro: Configuração de teclas. Elaborado por: Alexandre Luis Correa.
O módulo de configuração do jogo deve permitir que o usuário defina a correspondência desejada
entre os eventos e as respectivas ações. Poderíamos resolver esse problema definindo constantes
equivalentes a todos os eventos e ações do programa e, a partir dessas constantes, estabelecer uma
configuração, correlacionando os tipos de eventos com os códigos das ações.
O código a seguir apresenta o esqueleto dessa solução, em que a operação tratarEvento é executada
sempre que um evento de interface ocorrer. Essa operação obtém o código da ação associada ao evento e
invoca a operação correspondente do jogo.
public class InterfaceJogo {
private Jogo jogo;
// mapa de configuração evento -> ação do jogo
Map configuracao = new HashMap();
public InterfaceJogo (Jogo jogo) {
this.jogo = jogo;
}
public void setConfiguracaoJogo(Map configuracao) {
Padrões de Projetos de Software com Java
Marcio Quirino - 68
this.configuracao = configuracao;
}
public void tratarEvento (int evento) {
// acessa a tabela para obter o código da ação correspondente ao evento
int codigoAcao = getCodigoAcao(evento);
// executa a ação correspondente ao código
switch (codigoAcao) {
case IR_PARA_FRENTE: jogo.irParaFrente();
break;
case IR_PARA_TRAS: jogo.irParaTras();
break;
case PULAR: jogo.pular();
break;
}
}
private int getCodigoAcao (int evento) {
return configuracao.get(evento);
}
}
Essa solução cria um acoplamento entre o elemento que captura o evento disparado pela interface
com o usuário (InterfaceJogo) e o módulo que realiza as operações em resposta ao Jogo, além de conter
uma estrutura condicional não extensível. Imagine que o jogo tenha cinquenta comandos.
Consegue visualizar como essa estrutura condicional definida pelo comando switch ficaria enorme?
Solução do padrão Command
A solução proposta pelo padrão Command consiste em transformar cada ação em um objeto.
Portanto, em vez de as ações serem implementadas como operações de uma classe, cada ação é
implementada individualmente em uma classe. O diagrama a seguir apresenta a estrutura do padrão.
Diagrama de classes.
Cada ação é definida em uma classe correspondente ao participante ConcreteCommand, que
implementa a interface abstrata Command, na qual a operação genérica execute está definida. O
participante Client corresponde a um módulo cliente responsável por fazer a instanciação e a associação de
cada comando ao respectivo elemento Receiver, módulo que efetivamente realizará as ações associadas à
requisição. O participante Invoker corresponde ao elemento que dispara o comando, como um botão em
uma interface gráfica, por exemplo.
Padrões de Projetos de Software com Java
Marcio Quirino - 69
Veja, a seguir, a estrutura do código para a configuração dos eventos do jogo com a aplicação desse
padrão. Primeiro, criamos a interface genérica Comando e os comandos concretos correspondentes às
ações do jogo.
public interface Comando {
public abstract void executar();
}
public class Comando_IrParaFrente implements Comando {
private Jogo jogo;
public Comando_IrParaFrente (Jogo jogo) {
this.jogo = jogo;
}
public void executar() {
jogo.irParaFrente();
}
}
public class Comando_IrParaTras implements Comando {
private Jogo jogo;
public Comando_IrParaTras (Jogo jogo) {
this.jogo = jogo;
}
public void executar() {
jogo.irParaTras();
}
}
Note que o construtor de cada comando concreto recebe o objeto da classe Jogo para o qual a
requisição será passada. Portanto, Jogo corresponde ao participante Receiver do padrão.
Em seguida, vamos criar a configuração do jogo, associando os eventos gerados pela interface com
o usuário, isto é, teclas e botões do mouse, com os comandos. Note que podemos associar o mesmo
comando a diferentes eventos de interface, como ocorre com o comando pular. A classe ConfiguracaoJogo
corresponde ao participante Client do padrão.
public class ConfiguracaoJogo {
public static Map obterConfiguracaoPadrao (Jogo jogo) {
Comando comando_ParaFrente = new Comando_IrParaFrente(jogo);
Comando comando_ParaTras = new Comando_IrParaTras(jogo);
Comando comando_Pular = new Comando_Pular(jogo);
Map comandoMap = new HashMap();
comandoMap.put(TECLA_W, comando_ParaFrente);
comandoMap.put(TECLA_S, comando_ParaFrente);
comandoMap.put(TECLA_ESPACO, comando_Pular);
comandoMap.put(BOTAO_4, comando_Pular);
return comandoMap;
}
}
Por fim, vamos ver a nova implementação da classe InterfaceJogo correspondente ao participante
Invoker do padrão.
Essa classe recebeuma configuração dos códigos dos eventos de interface e seus respectivos
comandos. Note como, em comparação com a versão original, o método tratarEvento ficou desacoplado de
todos os possíveis comandos. Isso permite adicionarmos comandos ao jogo sem que esta classe precise
ser modificada.
public class InterfaceJogo {
private Map configuracao;
public void setConfiguracaoJogo(Map configuracao) {
this.configuracao = configuracao;
}
public void tratarEvento(int evento) {
Padrões de Projetos de Software com Java
Marcio Quirino - 70
// acessa a tabela para obter o comando correspondente ao evento e o
executa.
getComando(evento).execute();
}
private Comando getComando (int evento) {
return configuracao.get(evento);
}
}
public class AppExemplo {
public static void main (String[] args) {
Jogo jogo = new Jogo();
InterfaceJogo controlador = new InterfaceJogo();
controlador.setConfiguracaoJogo(ConfiguracaoJogo.obterConfiguracaoPadrao(jogo));
// ... código restante para colocar o jogo no ar
}
}
Consequências e padrões relacionados ao padrão Command
O padrão Command promove o desacoplamento entre o objeto que faz a requisição e o objeto que
realiza a operação requisitada. Cada comando passa a ser uma classe e, com isso, é possível reutilizar um
código comum entre os comandos e adicionar comandos sem precisar alterar classes já existentes.
Esse desacoplamento também resulta em maior facilidade para a implementação de testes unitários
automatizados, uma vez que podemos executar uma sequência de comandos de forma isolada do seu
mecanismo de disparo, como os eventos da interface gráfica com o usuário, por exemplo. A interface
Comando pode ser estendida adicionando-se as seguintes operações:
A. undo
✓ Responsável por desfazer as ações realizadas pelo comando.
B. redo
✓ Responsável por realizar novamente as ações desfeitas pela operação undo.
Como o comando é encapsulado em um objeto, ele pode armazenar as informações necessárias
para reverter ou repetir suas ações, tornando possível a implementação dessas operações.
Reflexão
Como um macro comando, isto é, uma sequência de comandos, pode ser implementado, aplicando-se
simultaneamente os padrões Command e Composite?
Padrão Iterator
Ao trabalhar no paradigma estruturado, é comum o uso de vetores para representar um conjunto de
valores do mesmo tipo, mas quando usamos a orientação a objetos, é mais indicado organizar conjuntos de
objetos por meio de coleções. Ao contrário dos vetores, que apresentam tamanho fixo, permitindo um acesso
indexado, as coleções trabalham com o acréscimo dinâmico de elementos, exigindo um método próprio para
acesso, e nesse ponto o padrão Iterator assume um papel muito importante. Aqui veremos como utilizar
esse padrão para efetuar tarefas comuns sobre as coleções como, por exemplo, exibir os dados de um
conjunto de entidades no terminal.
Intenção do padrão Iterator
O objetivo do padrão Iterator é permitir o acesso sequencial aos objetos de uma coleção, sem expor
a sua representação interna.
Problema resolvido pelo padrão Iterator
Suponha que você tenha que percorrer uma coleção de produtos armazenados em um ArrayList.
Você poderia escrever um código como o apresentado a seguir:
Padrões de Projetos de Software com Java
Marcio Quirino - 71
public class Exemplo {
public void imprimirProdutos(ArrayList produtos) {
for (Produto produto : produtos)
System.out.println (produto.getNome());
}
}
O problema dessa solução é que a classe Exemplo conhece a representação interna da coleção de
produtos, isto é, ela sabe que os produtos estão organizados em um ArrayList.
Atenção
No exemplo apresentado, caso tivéssemos outra coleção de produtos organizada em uma estrutura de mapa
indexado pelo código do produto, não poderíamos utilizar a classe Exemplo, pois ela só funciona com produtos
organizados em um ArrayList.
Solução do padrão Iterator
A solução proposta pelo padrão Iterator consiste em colocar a responsabilidade pelo percurso e pelo
acesso aos elementos de uma coleção em um objeto específico, denominado Iterator. A estrutura do padrão
está ilustrada no diagrama de classes a seguir.
O participante Aggregate representa uma coleção genérica cujos elementos podem ser percorridos
sequencialmente pelo conjunto de operações (First, Next, IsDone e CurrentItem) definido pelo participante
Iterator. O participante ConcreteAggregate representa uma coleção específica, que é responsável por criar
elementos do tipo ConcreteIterator capazes de percorrê-la.
O framework de estrutura de dados da linguagem Java implementa esse padrão. Coleções como
ArrayList, LinkedList, HashSet e TreeSet são descendentes da classe genérica Collection. Nesse caso, as
coleções específicas correspondem ao participante ConcreteAggregate, enquanto a classe Collection
corresponde ao participante Aggregate. Em Java, a interface genérica Iterator define um conjunto de
operações um pouco diferente daquele definido na estrutura do padrão:
A. hasNext
✓ Verifica se existe um próximo elemento ou se o cursor já está posicionado no último elemento
da coleção.
B. next
✓ Retorna o próximo elemento da coleção. Na primeira chamada, ele retorna o primeiro
elemento da coleção.
Padrões de Projetos de Software com Java
Marcio Quirino - 72
C. remove
✓ Remove um elemento da coleção.
Cada coleção define uma operação Iterator que retorna um objeto ConcreteIterator capaz de
percorrê-la. O diagrama de classes a seguir ilustra os iteradores correspondentes às coleções ArrayList
(ArrayIterator), LinkedList (ListIterator), HashSet (KeyIterator) e TreeSet (ValueIterator).
Veja, a seguir, um exemplo de utilização desse padrão em Java. O método removerItensSemEstoque
recebe uma coleção de produtos. Ele solicita um Iterator à coleção e utiliza as operações hasNext e next
para percorrê-la, removendo os produtos cuja quantidade em estoque for zero.
public void removerItensSemEstoque (Collection produtos) {
// coleção de produtos cria e retorna um iterator capaz de percorrer a coleção
Iterator iterator = produtos.iterator();
// coleção de produtos é percorrida com as operações hasNext e next
while (iterator.hasNext()) {
Produto produto = iterator.next();
if (Produto.getQuantidadeEstoque() == 0) {
itens.remove(item);
}
}
}
Note que esse método funciona com qualquer coleção, isto é, ArrayList, LinkedList, HashSet ou
TreeSet.
Consequências e padrões relacionados ao padrão Iterator
O principal benefício do padrão Iterator é permitir que os módulos clientes possam percorrer
sequencialmente os elementos de uma coleção de forma independente da sua representação interna. Outro
benefício é a possibilidade de haver diversas instâncias de Iterator ativas para uma mesma coleção, uma
vez que cada Iterator guarda o seu próprio estado, isto é, a posição corrente na coleção.
Além disso, é possível implementar iteradores com diferentes formas de percurso. Por exemplo,
podemos percorrer uma árvore binária em pré-ordem, ordem simétrica e pós-ordem. Cada forma de percurso
pode ser definida em uma implementação específica da interface Iterator.
Atenção
O padrão Iterator é frequentemente utilizado com o padrão Factory Method, uma vez que cada método Iterator
do participante ConcreteAggregate é um Factory Method responsável por instanciar o respectivo ConcreteIterator.
Padrões de Projetos de Software com Java
Marcio Quirino - 73
2. Padrões de projeto comportamentais Mediator, Memento e
StrategyPadrão Mediator
Nem sempre uma negociação ocorre de forma simples, e quando a situação se torna mais crítica,
um mediador independente pode ser a única solução. O mesmo ocorre em termos de softwares, quando
diversos módulos precisam trabalhar em conjunto para a realização de tarefas específicas, mas de forma
que não sejam feitas chamadas diretas entre eles, evitando aumentar o acoplamento. Por exemplo, um
processo de compra na Web envolve a utilização de vários módulos, nos quais o carrinho precisa de
informações do estoque e o pagamento demanda ações logísticas. Para impedir que esses módulos se
comuniquem diretamente, as chamadas podem ser feitas via classe Mediator.
Intenção do padrão Mediator
O padrão Mediator encapsula a forma de interação entre um conjunto de objetos, com o objetivo de
evitar que eles tenham que referenciar uns aos outros explicitamente.
Problema resolvido pelo padrão Mediator
Em um sistema de comércio eletrônico, quando o cliente efetua o pagamento, a compra deve ser
confirmada, o processo de logística de entrega deve ser disparado e um e-mail de confirmação do pedido
deve ser enviado para o cliente.
Imagine que um desenvolvedor tenha dado a solução esquematizada no diagrama de sequência a
seguir.
Nessa solução, o módulo Pagamento, após realizar o pagamento da compra, chama a operação
confirmar do módulo Compra, que, por sua vez, chama a operação prepararEntrega do módulo Logística e
a operação enviarConfirmacaoCompra do módulo SAC, responsável por enviar um e-mail para o usuário
com os dados da compra.
Você consegue visualizar o alto acoplamento entre as classes na realização do processo de
fechamento da compra? Imagine que você precise inserir uma etapa nesse processo, como a baixa no
estoque, por exemplo. Você teria que adicionar uma dependência no módulo Compra ou no módulo
Logística, que ficaria responsável por chamar uma operação do módulo Estoque.
Como simplificar interações complexas entre os objetos, com o objetivo de reduzir o acoplamento
entre eles e permitir a criação de novas interações sem que esses objetos precisem ser alterados? Essa é
a essência do problema que o padrão Mediator visa solucionar.
Solução do padrão Mediator
A estrutura da solução proposta pelo padrão Mediator está representada no diagrama de classes a
seguir:
Padrões de Projetos de Software com Java
Marcio Quirino - 74
A solução consiste em centralizar as comunicações entre os objetos em um elemento denominado
mediador. O participante Mediator define uma interface padrão que cada objeto utilizará para se comunicar
com os demais envolvidos em uma interação. Esses objetos formam a hierarquia definida pelo participante
Colleague. O participante ConcreteMediator, por sua vez, corresponde a um elemento concreto que mantém
referências para todos os objetos cujas interações ele precisará mediar.
Vamos modificar a solução do problema do comércio eletrônico aplicando o padrão Mediator. As
classes Pagamento, Compra, Logistica e SAC correspondem ao participante ConcreteColleague e são
especializações da classe genérica Colleague, na qual está definida uma referência ao objeto mediador.
Note que a classe Pagamento notifica o mediador de que o pagamento foi concluído. Da mesma forma, a
classe Compra notifica o mediador de que a compra foi encerrada. Portanto, esses objetos passam a se
comunicar apenas com o mediador. Os módulos Pagamento e Conta apenas notificam o evento que ocorreu,
ficando a cargo do mediador definir o encaminhamento que deve ser dado a cada evento.
public abstract class Colleague {
// Todo objeto da interação possui uma referência para o mediador
protected MediadorCompra mediador;
public Colleague(MediadorCompra mediador) {
this.mediador = mediador;
}
}
public class Pagamento extends Colleague {
public Pagamento(MediadorCompra mediador) {
super(mediador);
}
public void realizarPagamento(int idCompra) {
// código para realizar pagamento
mediador.pagamentoConcluido(idCompra); // notifica o mediador
}
}
public class Compra extends Colleague {
public Compra(MediadorCompra mediador) {
super(mediador);
}
public void confirmar(int idCompra) {
// código para confirmação da compra
mediador.pedidoConfirmado(); // notifica o mediador
}
}
public class Logistica extends Colleague {
public Logistica(MediadorCompra mediador) {
super(mediador);
}
public void prepararEntrega(CompraVO dadosCompra) {
// código que inicia o processo de logística de entrega da compra
}
Padrões de Projetos de Software com Java
Marcio Quirino - 75
}
public class SAC extends Colleague {
public SAC(MediadorCompra mediador) {
super(mediador);
}
public void enviarEmailConfirmacaoCompra(CompraVO dadosCompra) {
// código para enviar o email de confirmação da compra para o cliente
}
}
A implementação do mediador é ilustrada esquematicamente a seguir. A interface MediadorCompra
define todas as notificações que os componentes participantes da interação podem enviar para o objeto
mediador. Essa interface é implementada pela classe MediadorCompraSimples, que representa uma
implementação simples das interações entre os objetos. Note que outras classes de mediação de compra
podem ser implementadas, representando diferentes processos de interação entre os participantes.
A classe MediadorCompraSimples corresponde ao participante ConcreteMediator. Ela possui uma
referência para cada objeto participante das interações (pagamento, compra, logística e sac), recebe os
eventos enviados por cada participante e dispara a execução de operações em resposta a cada evento.
public interface MediadorCompra {
void pagamentoConcluido(int idCompra);
void pedidoConfirmado();
}
public class MediadorCompraSimples implements MediadorCompra {
private Pagamento pagamento;
private Compra compra;
private Logistica logistica;
private SAC sac;
public MediadorCompraSimples(Pagamento pagamento, Compra compra, Logistica logistica,
SAC sac) {
this.pagamento = pagamento;
this.compra = compra;
this.logistica = logistica;
this.sac = sac;
}
public void pagamentoConcluido(int idCompra) {
// mediador invoca operações da compra ao receber a notificação de pagamento.
compra.confirmar(idCompra);
}
public void pedidoConfirmado() {
// mediador invoca operações de logística e do sac ao receber a confirmação.
CompraVO dadosCompra = prepararDadosCompra(compra);
logistica.prepararEntrega(dadosCompra);
sac.enviarEmailConfirmacaoCompra(dadosCompra);
}
private CompraVO prepararDadosCompra(Compra compra) {
CompraVO dadosCompra = new CompraVO();
// código para carga de todos os dados da compra estaria aqui
return dadosCompra;
}
}
Consequências e padrões relacionados ao padrão Mediator
O padrão Mediator gera uma solução de menor acoplamento para interações complexas entre
objetos, pois ele substitui uma estrutura de interação do tipo muitos para muitos para uma estrutura um para
muitos, que são mais fáceis de entender e manter. Por outro lado, o controle fica centralizado no mediador.
Um cuidado especial deve ser tomado na implementação do mediador para evitar que ele se torne um
componente monolítico com grande complexidade de manutenção.
Padrões de Projetos de Software com Java
Marcio Quirino - 76
O mediador deve ser apenas um concentrador de eventos e um coordenador de execução, ficando
a lógica do processamento distribuída pelos elementos a ele conectados.
Essepadrão é muito utilizado na implementação de componentes de interface com o usuário, em
que um mediador centraliza a recepção e o tratamento dos eventos gerados pelos componentes da interface,
como botões, campos de texto e listas de seleção, por exemplo. Em uma tela em que o usuário entra um
endereço, a modificação do campo CEP pode acarretar a atualização dos campos logradouro, cidade e
estado. Um mediador que receba a notificação de que o CEP foi alterado e promova a atualização dos
demais campos envolvidos é uma solução normalmente empregada nesse contexto.
Atenção
Como a comunicação entre os participantes e o mediador se dá por meio de notificações de eventos, é comum
o Mediator ser aplicado em conjunto com o padrão Observer. Nesse caso, o mediador corresponde ao participante
Observer, enquanto os objetos notificadores correspondem ao participante Subject do padrão Observer.
Padrão Memento
Não são raras as situações em que precisamos desfazer algo, motivo pelo qual todos adoram a
combinação de teclas CTRL e Z, presente em diversos aplicativos. O padrão Memento tem como objetivo
viabilizar esse tipo de comportamento, empilhando instantâneos do estado do objeto, de forma a permitir a
recuperação dos estados anteriores. Como um exemplo prático, supondo que você precise definir um
aplicativo de modelagem tridimensional, o Memento permitiria ao profissional de modelagem desfazer suas
últimas operações, uma funcionalidade essencial para dar maior liberdade e flexibilidade ao processo
criativo.
Intenção do padrão Memento
Memento é um padrão que permite capturar o estado interno de um objeto, sem quebrar o seu
encapsulamento, de forma que esse estado possa ser restaurado posteriormente.
Problema resolvido pelo padrão Memento
Suponha que você esteja desenvolvendo uma aplicação em que seja necessário desfazer o efeito
produzido por uma ou mais operações em determinado objeto ou em um conjunto de objetos, procedimento
conhecido como undo. Para isso, é necessário guardar o estado do objeto anterior à execução das
operações que precisarão ser desfeitas.
Veja a classe Pedido cujo código é listado a seguir. Ela define o número do pedido, o número do item
a ser utilizado na criação de um novo item e a lista de itens do pedido. Apenas a classe Pedido tem acesso
ao array de itens. Portanto, a adição de novos itens ao pedido deve ser realizada por meio da operação
adicionarItem.
public class Pedido {
private int numero;
private List itens;
private int numeroItemCorrente;
public Pedido(int numero) {
this.numero = numero;
itens = new ArrayList();
numeroItemCorrente = 1;
}
public void adicionarItem(int qtde, String codigoProduto) {
itens.add (new ItemPedido (++numeroItemCorrente, qtde, codigoProduto));
}
public int getNumero() {
return numero;
}
public void setNumero(int numero) {
this.numero = numero;
Padrões de Projetos de Software com Java
Marcio Quirino - 77
}
public Iterator getItens() {
return itens.iterator();
}
}
Agora pense como você implementaria uma funcionalidade que permitisse o usuário desfazer as
últimas operações de adição de itens ao pedido. E se oferecêssemos um recurso que possibilitasse o usuário
interromper sua compra e voltar mais tarde, sem perder o conteúdo já adicionado ao pedido?
O padrão Memento serve para nos ajudar em situações como essas.
Solução do padrão Memento
A estrutura da solução proposta pelo padrão Memento está representada no diagrama de classes a
seguir:
No código a seguir, implementamos um memento para objetos da classe Pedido. A classe
PedidoMemento representa o participante Memento do padrão, armazenando todas as informações
necessárias para a restauração de um objeto Pedido.
public class PedidoMemento {
private int numero;
private int numeroCorrente;
private List itens;
public PedidoMemento(int numeroPedido, int numeroCorrente, List
itensOrigem) {
this.numero = numeroPedido;
this.numeroCorrente = numeroCorrente;
this.itens = new ArrayList();
for (ItemPedido item : itensOrigem) {
this.itens.add(
new ItemPedidoMemento (item.getNumero(), item.getQuantidade(),
item.getCodigoProduto()));
}
}
public int getNumero() {
return numero;
}
public int getNumeroCorrente() {
return numeroCorrente;
}
public List getItens() {
return itens;
}
}
A classe Pedido representa o participante Originator do padrão, pois é o seu estado que deve ser
armazenado e restaurado por meio do uso de mementos. Veja o próximo código:
public class Pedido {
private int numero;
private List itens;
private int numeroCorrente;
public Pedido(int numero) {
this.numero = numero;
itens = new ArrayList();
Padrões de Projetos de Software com Java
Marcio Quirino - 78
numeroCorrente = 1;
}
public PedidoMemento criarMemento() {
return new PedidoMemento(numero, numeroCorrente, itens);
}
public void restaurarMemento(PedidoMemento memento) {
this.numero = memento.getNumero();
this.numeroCorrente = memento.getNumeroCorrente();
itens = new ArrayList();
for (ItemPedidoMemento itemMemento : memento.getItens()) {
itens.add(new ItemPedido(itemMemento.getNumero(), itemMemento.getQuantidade(),
itemMemento.getCodigoProduto()));
}
}
O código anterior ilustra duas operações adicionadas à classe Pedido original:
A. Operação criarMemento
✓ Salva o estado do pedido em uma instância de PedidoMemento.
B. Operação restaurarMemento
✓ Restaura o estado do pedido a partir do objeto PedidoMemento recebido como parâmetro.
Veja no código a seguir um exemplo de implementação de uma operação undo utilizando um objeto
da classe Pedido. Note que esse programa apenas guarda uma referência para o memento criado e
retornado pelo objeto pedido, repassando-o quando for necessário restaurar o estado desse pedido. Em
nenhum momento, o programa exemplo chama operações do objeto memento.
public class Exemplo {
public static void main(String[] args) {
Pedido pedido = new Pedido(1);
pedido.adicionarItem(1, '1234');
pedido.adicionarItem(2, '2345');
// salva o estado atual do pedido, com dois itens
PedidoMemento memento = pedido.criarMemento();
// adiciona o terceiro item ao pedido
pedido.adicionarItem(3, '3456');
System.out.println(pedido.getItens().size()); // imprime o valor 3
// restaura a situação salva do pedido, com dois itens apenas
pedido.restaurarMemento(memento);
System.out.println(pedido.getItens().size()); // imprime o valor 2
}
}
Consequências e padrões relacionados ao padrão Memento
O padrão Memento facilita a implementação de problemas nos quais precisamos desfazer certas
modificações de estado em objetos decorrentes da execução de operações ou implementar algum
mecanismo de checkpoint/restart, em que interrompemos o processamento para retomá-lo posteriormente
do ponto onde paramos.
A implementação de um memento pode ser custosa em situações nas quais exista uma grande
quantidade de informações para armazenar e posteriormente restaurar, especialmente quando envolver um
objeto que tenha uma grande rede de objetos relacionados. Além disso, há peculiaridades na implementação
do padrão Memento em algumas linguagens. A seguir, temos dois exemplos:
A. Java
✓ Dificulta a definição da interface do memento, de formaque somente o originador tenha
acesso.
B. C++
✓ Permite uma definição mais rigorosa por meio da utilização da palavra reservada friend.
Padrões de Projetos de Software com Java
Marcio Quirino - 79
Esse padrão é frequentemente utilizado com o padrão Command, quando este implementar um
mecanismo para desfazer um comando (undo), pois, para isso, é necessário guardar o estado anterior à sua
execução, o que pode ser feito com o uso do padrão Memento.
Padrão Strategy
O comportamento estratégico envolve uma tomada de decisão a partir da análise de condições
ambientais que favoreçam o melhor conjunto de operações. Será por meio do padrão Strategy que iremos
tornar nossos sistemas mais assertivos, agrupando um conjunto de operações em cada classe de estratégia,
e invocando a classe que mais se adequa ao contexto de execução do aplicativo. Por exemplo, um sistema
de detecção de vírus pode decidir entre manter, remover ou colocar um arquivo em quarentena, tendo como
base a análise de código efetuada por um módulo de inteligência artificial.
Intenção do padrão Strategy
O padrão Strategy define uma família de algoritmos, encapsulando-os em objetos e permitindo que
eles possam ser utilizados de forma intercambiável, ou seja, o algoritmo específico pode ser trocado sem
que o módulo usuário desse algoritmo precise ser alterado.
Problema resolvido pelo padrão Strategy
O padrão Strategy é aplicável em situações nas quais existam diferentes algoritmos para gerar
determinado resultado. Um exemplo desse tipo de situação é o cálculo dos juros de um título público. Você
sabia que esse é um conhecimento fundamental para quem trabalha no mercado financeiro?
Existem diferentes métodos para calcular os juros de um título para negociação em determinada
data. Alguns exemplos de métodos são:
• Regressão linear múltipla
• Bootstrap
• Interpolação polinominal splines cúbicos
Imagine que você esteja desenvolvendo um módulo que calcula a taxa de juros de um título para
negociação no dia seguinte. Uma solução frequentemente encontrada consiste em concentrar toda a lógica
de cálculo em um único módulo, como ilustrado pela estrutura de código apresentada a seguir. É importante
observar que esta é uma estrutura bastante simplificada da implementação real do problema, pois
abstraímos a complexidade matemática envolvida.
public class CurvaJuros {
private List pontos; // pontos que formam a curva de juros
// calcular a taxa de juros de um título para uma determinada data
public BigDecimal taxaJuros (Titulo titulo, Date data, TipoAlgoritmo tipo) {
switch (tipo) {
case TipoAlgoritmo.REGRESSAO_MULTIPLA:
return taxaPorRegressaoMultipla(titulo, data);
break;
case TipoAlgoritmo.BOOTSTRAP:
return taxaPorBootstrap(titulo, data);
break;
case TipoAlgoritmo.SPLINE:
return taxaPorSpline(titulo, data);
break;
}
}
private BigDecimal taxaPorRegressaoMultipla (Titulo titulo, Date data) {
// implementação do algoritmo por regressão múltipla
// acessa os dados da curva definidos no atributo pontos
}
private BigDecimal taxaPorBootstrap (Titulo titulo, Date data) {
// implementação do algoritmo pelo método bootstrap
// acessa os dados da curva definidos no atributo pontos
Padrões de Projetos de Software com Java
Marcio Quirino - 80
}
private BigDecimal taxaPorSpline (Titulo titulo, Date data) {
// implementação do algoritmo pelo método splines cúbicos
// acessa os dados da curva definidos no atributo pontos
}
}
Esse tipo de solução possui dois problemas. Você já identificou quais são eles? Vejamos:
A. Problema 1
✓ Para adicionar novos algoritmos, temos que abrir o módulo TituloPublico para adicionar um
novo método e um novo tipo ao switch/case do método taxaJuros, violando o princípio Open
Closed, um dos princípios SOLID de projeto.
B. Problema 2
✓ Não é possível reutilizar o algoritmo para cálculo de outros valores que não sejam taxas de
juros. Esses algoritmos são métodos matemáticos aplicáveis a outros problemas. Como
podemos separar os algoritmos das classes de domínio em que eles são aplicados?
Um problema similar ao problema 2 ocorre com os algoritmos de ordenação de dados, pois existem
diferentes métodos de ordenação (Bubble Sort, Quick Sort, Merge Sort, Heap Sort, por exemplo) aplicáveis
a diferentes tipos de dados (produtos, pessoas, números etc.).
Solução do padrão Strategy
A estrutura da solução proposta pelo padrão Strategy está representada no diagrama de classes a
seguir.
A ideia central consiste em separar cada algoritmo em uma classe, fazendo com que todas as classes
específicas implementem uma interface comum, representada pelo participante Strategy. O participante
Context define o contexto que proverá o algoritmo com os dados necessários para o processamento. Note
que o contexto mantém uma referência para a interface genérica, não dependendo de qualquer
implementação específica. Com isso, podemos adicionar novos algoritmos sem precisarmos modificar a
classe que define o contexto para a sua aplicação.
O código a seguir mostra como podemos implementar o problema do cálculo de juros de um título
com a aplicação desse padrão. A interface CalculadoraJuros corresponde ao participante Strategy e define
uma interface genérica para o cálculo da taxa a partir do título, da data para a qual a taxa será calculada e
dos pontos da curva. Cada algoritmo é definido em uma classe que implementa essa interface,
correspondendo ao participante ConcreteStrategy definido na estrutura do padrão.
public interface CalculadoraJuros {
BigDecimal taxaJuros (Titulo titulo, Date data, List pontos);
}
public class CalculadoraJurosRegressaoMultipla implements CalculadoraJuros {
Padrões de Projetos de Software com Java
Marcio Quirino - 81
public BigDecimal taxaJuros (Titulo titulo, Date data, List pontos) {
// implementação do algoritmo por regressão múltipla
// acessa os dados da curva recebidos no parâmetro pontos
}
}
public class CalculadoraJurosBootstrap implements CalculadoraJuros {
public BigDecimal taxaJuros (Titulo titulo, Date data, List pontos) {
// implementação do algoritmo pelo método bootstrap
// acessa os dados da curva recebidos no parâmetro pontos
}
}
public class CalculadoraJurosSpline implements CalculadoraJuros {
public BigDecimal taxaJuros (Titulo titulo, Date data, List ponto) {
// implementação do algoritmo pelo método splines cúbicos
// acessa os dados da curva recebidos no parâmetro pontos
}
}
Perceba que os algoritmos, antes implementados em operações de uma única classe, foram
transformados em classes, todas implementando o mesmo conjunto de operações. A classe CurvaJuros
passou a receber o algoritmo de cálculo como parâmetro, isto é, o algoritmo a ser utilizado passou a ser
injetado dinamicamente no objeto, permitindo que ele trabalhe com qualquer algoritmo que implemente a
interface genérica CalculadoraJuros.
Portanto, a classe CurvaJuros corresponde ao participante Context do padrão, pois ela estabelece o
contexto com os dados necessários para o processamento do algoritmo. É importante observar que essa
injeção pode ser feita a cada chamada da operação taxaJuros, como mostrado no exemplo, ou apenas no
construtor da classe CurvaJuros, o que faria com que cada instância de curva sempre trabalhasse com o
algoritmo configurado na sua criação.
publicclass CurvaJuros {
private List pontos;
// calcular a taxa de juros de um título para uma determinada data
public BigDecimal taxaJuros (Titulo titulo, Date data, CalculadoraJuros
calculadoraJuros) {
return calculadoraJuros.taxaJuros (titulo, data, pontos);
}
}
Veja no código a seguir como um módulo relacionado à classe CurvaJuros pode solicitar o cálculo
da taxa, passando o algoritmo desejado para a sua execução.
public class ClienteExemplo {
private CurvaJuros curva;
public ClienteExemplo() {
curva = new CurvaJuros();
// processo de construção dos pontos da curva (omitido para simplificação)
}
public BigDecimal obterTaxa (Titulo titulo, Date data) {
// neste contexto, a taxa é calculada utilizando o método splines cúbicos
return curva.taxaJuros (titulo, data, new CalculadoraJurosSpline());
}
}
Consequências e padrões relacionados ao padrão Strategy
O padrão Strategy oferece algumas vantagens:
1. Definição de família de algoritmos
✓ Permite a definição de uma família de algoritmos, que podem ser utilizados e configurados
de forma flexível.
2. Passos comuns na superclasse
Padrões de Projetos de Software com Java
Marcio Quirino - 82
✓ Permite a implementação dos passos em comum dos diferentes algoritmos na superclasse,
evitando a duplicação de código.
3. Simplificação das estruturas
✓ Evita a criação de estruturas condicionais complexas no contexto da aplicação dos algoritmos,
substituindo comandos switch/case por chamadas polimórficas de operações.
Por outro lado, o padrão Strategy expõe as diferentes opções de algoritmo para os clientes. Portanto,
o uso desse padrão é mais indicado para as situações nas quais o cliente conheça e precise escolher o
algoritmo mais apropriado.
O algoritmo a ser utilizado pode ser parametrizado em uma configuração da aplicação, utilizando-se
um padrão de criação (Factory Method ou Abstract Factory), ou ainda o recurso de injeção de dependências,
para generalizar o processo de instanciação do algoritmo específico a ser utilizado.
Atenção
O padrão Strategy, por gerar objetos sem estado, pois os dados de que o algoritmo precisa estão definidos na
classe contexto, pode ser implementado em combinação com o padrão Flyweight, de modo que uma única instância
de cada algoritmo seja compartilhada pelos vários contextos. Em nosso exemplo, se fossem criadas 50 instâncias de
CurvaJuros, bastaria instanciar um único objeto de cada algoritmo, caso adicionássemos o padrão Flyweight à solução.
3. Padrões de projeto comportamentais Observer, Visitor e
State
Padrão Observer
Atualmente o padrão Observer é utilizado em qualquer plataforma de desenvolvimento, como no
relacionamento entre modelo e visualização do Android, ou nas chamadas HTTP assíncronas do Angular
via RxJS.
Segundo o padrão Observer, uma fonte de dados pode ser assinada por um conjunto de
observadores, e qualquer modificação nos dados irá gerar uma notificação para os assinantes, permitindo a
atualização de elementos associados em tempo real. Como exemplo simples, poderíamos construir uma
planilha de gastos, em que um campo com o valor total e um gráfico agrupado por categoria seriam
atualizados a cada novo lançamento na planilha.
Intenção do padrão Observer
O padrão Observer define uma relação de dependência entre objetos, de modo a garantir que,
quando alguma modificação no estado de determinado objeto ocorrer, todos os objetos dependentes sejam
notificados e atualizados automaticamente.
Problema resolvido pelo padrão Observer
Imagine que você esteja desenvolvendo um software para acompanhamento de vendas de uma
empresa e que os dados de venda por produto e região sejam apresentados em três painéis
simultaneamente.
O primeiro painel é apresentado em tabela:
Produto Região 1 Região 2 Região 3 Total
A 40 30 100 170
B 120 80 70 270
C 80 50 90 220
Total 240 160 260 660
Tabela: Painel 1. Elaborada por: Alexandre Luis Correa.
Os painéis 2 e 3 são apresentados em gráficos:
Padrões de Projetos de Software com Java
Marcio Quirino - 83
Os três painéis apresentam dados oriundos de um sumário de vendas. Quando qualquer dado desse
sumário mudar, os três painéis devem ser atualizados.
O problema resolvido pelo padrão Observer consiste em manter a consistência entre objetos
relacionados de modo que, se o estado de um objeto mudar, todos os objetos afetados por essa mudança
sejam notificados e atualizados.
Solução do padrão Observer
O padrão Observer possui a estrutura definida no diagrama de classes a seguir:
Vamos analisar cada participante do diagrama:
A. Subject
✓ O participante Subject define uma interface para registro (attach) e desligamento (detach) de
observadores que devem ser notificados quando houver uma mudança no estado de um
objeto concreto. Os observadores são armazenados em uma coleção mantida pelo Subject.
B. Observer
✓ O participante Observer define uma interface para o recebimento das notificações enviadas
pelo Subject.
C. ConcreteSubject
✓ O participante ConcreteSubject corresponde a um elemento específico da aplicação que está
sendo construída cujo estado, representado pelo atributo subjectState, é do interesse de um
conjunto de observadores que serão notificados quando esse estado mudar.
D. ConcreteObserver
✓ O participante ConcreteObserver mantém uma referência para o objeto ConcreteSubject,
armazenando ou apresentando dados, representados pelo atributo observerState, que devem
se manter consistentes com o estado desse objeto. Ele implementa a interface de
recebimento de notificação enviada pelo Subject (operação update), sendo responsável por
Padrões de Projetos de Software com Java
Marcio Quirino - 84
obter o novo estado do ConcreteSubject, por meio das operações representadas pela
operação GetState do participante Subject.
O diagrama de sequência a seguir ilustra as interações entre os participantes da solução.
Inicialmente, os objetos observadores devem se registrar no objeto Subject, por meio da operação attach.
O estado de um objeto ConcreteSubject pode ser alterado, por meio das suas operações modificadoras,
representadas genericamente pela operação setState. A implementação dessas operações modificadoras
deve chamar a operação notify, definida para todo objeto do tipo Subject. A operação notify, por sua vez,
deve invocar a operação update de todos os objetos observers registrados previamente. A implementação
da operação update em cada ConcreteObserver deve obter o novo estado do objeto ConcreteSubject,
invocando as operações de consulta representadas pela operação getState.
Em Java, esse padrão pode ser implementado utilizando as classes disponíveis nas bibliotecas da
linguagem. Com o Java 8, uma das implementações possíveis consiste em definir as classes notificadoras
como subclasses de java.util.Observable, enquanto os observadores devem implementar a interface
java.util.Observer.
O código a seguir apresenta um exemplo de implementação desse padrão. A classe Ponto
desempenha o papel de Subject. Nesse caso, Ponto é uma subclasse de Observable, herdando a
implementação da gestão da lista de observadores e o método para notificação. Observe que todas as
mudanças relevantes, presentes nos métodos setX e setY, chamam duas operações da superclasse
Observable: setChanged e notifyObservers, que notificam todos os objetos observadores de que houve uma
mudança no valor de um atributo.
public class Ponto extends Observable {
private int x;
private int y;
public Ponto(int a, int b) {
this.x = a;
this.y = b;
}
public int getX() {
Padrões de Projetos de Software com Java
Marcio Quirino - 85
return x;
}
public void setX(int x) {this.x = x;
setChanged(); // chama operação definida na superclasse Observable
notifyObservers(); // chama operação definida na superclasse Observable
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
setChanged(); // chama operação definida na superclasse Observable
notifyObservers(); // chama operação definida na superclasse Observable
}
}
Em seguida, definimos duas classes que desempenham o papel de observadores de modificações
nas instâncias da classe Ponto. As duas classes implementam a operação update, definida na classe
Observable, que é chamada pela operação notifyObservers. Assim, cada observador pode recuperar os
dados atualizados do ponto e fazer qualquer tipo de atualização, como exibi-los na tela, por exemplo.
public class Observador1 implements Observer {
public void update (Observable o, Object o1) {
if (o instanceof Ponto) {
Ponto exemplo = (Ponto) o;
System.out.println('Observador 1 => y = ' + exemplo.getY());
}
}
}
public class Observador2 implements Observer {
public void update(Observable o, Object o1) {
if (o instanceof Ponto) {
Ponto exemplo = (Ponto) o;
System.out.println('Observador 2 => x = ' + exemplo.getX());
System.out.println('Observador 2 => y = ' + exemplo.getY());
}
}
}
Vamos ver agora como o objeto subject e os observadores devem ser conectados para que o
mecanismo funcione. O código a seguir apresenta um exemplo em que uma instância da classe Ponto é
criada e dois observadores são adicionados a ela por meio da operação addObserver. A partir daí, toda vez
que uma operação setX ou setY for executada, a operação update de cada observador será
automaticamente chamada pelo mecanismo de notificação.
public class ExemploObserver {
public static void main (String[] args) {
Ponto subject = new Ponto(2, 5);
Observador1 obs1 = new Observador1();
Observador2 obs2 = new Observador2();
subject.addObserver(obs1); // equivalente à operação attach definida no padrão
subject.addObserver(obs2);
subject.setX(10); // modifica o valor de x e notifica os observadores
subject.setY(10); // modifica o valor de y e notifica os observadores
subject.setX(20); // modifica o valor de x e notifica os observadores
}
}
Consequências e padrões relacionados ao Observer
O padrão Observer permite que os objetos detentores de informação relevante possam notificar que
ocorreram modificações nessa informação aos possíveis interessados. Isso é feito por meio de uma interface
genérica que facilita a adição de novos interessados. Esse padrão é especialmente utilizado na
Padrões de Projetos de Software com Java
Marcio Quirino - 86
implementação de elementos de interface gráfica com o usuário que, além de gerarem eventos que precisam
ser notificados para outros elementos, precisam ficar sincronizados com a sua fonte de dados.
Uma consequência negativa desse padrão é que a solução proposta pode dar origem a muitas
chamadas da operação update. Imagine uma atualização de diversas informações de um Subject com
dezenas de observadores registrados. Nessa situação, seriam geradas centenas de chamadas da operação
update. Portanto, certos problemas podem demandar a implementação de um protocolo específico de
atualização, indicando quais modificações devem ser notificadas para cada observador e em que momento.
Atenção
O padrão Observer pode ser utilizado em conjunto com o padrão Mediator, especialmente quando certas
modificações no Subject gerarem uma lógica complexa de atualizações em outros objetos. Em vez de estabelecermos
diversas conexões diretas entre os observadores e os subjects, podemos inserir um mediador que centralize as
notificações enviadas pelos subjects e coordenar a lógica de atualização dos diversos observadores.
Padrão Visitor
Segundo a definição do padrão Visitor, ele visa promover a separação entre uma família de objetos
e os algoritmos que serão utilizados, permitindo que novas funcionalidades sejam definidas sem a
necessidade de modificar os objetos. Por exemplo, você poderia definir uma interface Visitante que aceitasse
planilhas, XML e JSON, implementando na classe GeradorHTML, que transformaria as informações em
tabelas HTML, e na classe GeradorPDF, para gerar arquivos PDF a partir dos dados fornecidos, e,
posteriormente, poderia adicionar a classe Base64Transform, que retornaria o conteúdo codificado em
Base64, evoluindo o sistema sem afetar as funcionalidades definidas previamente.
Intenção do padrão Visitor
O padrão Visitor permite a definição de novas operações em uma hierarquia de objetos sem que haja
a necessidade de modificar as classes dessa hierarquia.
Problema resolvido pelo padrão Visitor
Imagine que você esteja desenvolvendo um sistema para a área financeira que permite a
parametrização de alguns cálculos por meio de expressões.
Por exemplo, a expressão a + (10 * b) – (c / 5) pode ser representada pela seguinte estrutura de
objetos:
Essa estrutura de objetos pode ser percorrida para diferentes finalidades, tais como: verificar se a
expressão é válida, calcular o valor da expressão, imprimir a expressão em formato texto, ou imprimir a
expressão em notação polonesa.
Padrões de Projetos de Software com Java
Marcio Quirino - 87
Para cumprir todas essas finalidades, podemos acrescentar aos objetos dessa estrutura operações
que possibilitem a validação de uma expressão, o cálculo de valor e assim por diante. Entretanto, sempre
que quisermos acrescentar uma nova finalidade, teremos que modificar todas as classes dessa estrutura.
O padrão Visitor nos permite acrescentar novas funcionalidades sem precisar modificar as classes
que representam a estrutura fundamental dos objetos que compõem a expressão.
Solução do padrão Visitor
A estrutura da solução proposta pelo padrão Visitor é apresentada no diagrama UML a seguir:
A solução consiste em separar os elementos que formam uma estrutura hierárquica e os algoritmos
que realizam operações sobre essa estrutura em duas famílias de classes. Vamos analisar dois participantes
da solução apresentada:
A. Visitor
✓ O participante Visitor define uma família de operações, que podem aplicadas a cada um dos
elementos concretos da estrutura. Para cada classe concreta da estrutura, correspondente
ao participante ConcreteElement, deve ser definida uma operação visit,
que recebe como parâmetro um objeto da respectiva classe. Cada família de operações é
definida em um ConcreteVisitor específico.
B. Element
✓ O participante Element corresponde à classe mais genérica da estrutura hierárquica dos
elementos, na qual é definida a operação accept, que recebe um visitor como parâmetro e
chama a operação visitConcreteElement correspondente à sua classe, passando o próprio
objeto que está sendo visitado como argumento.
O código a seguir, ilustra a implementação do problema das expressões aritméticas, utilizando o
padrão Visitor. Para simplificar, vamos mostrar apenas somas de números inteiros. Cada número é uma
instância da classe NumeroInteiro, enquanto cada operador de soma é uma instância da classe OpSoma.
Todo operador aritmético herda da classe OperadorAritmetico que define dois operandos, um à esquerda e
Padrões de Projetos de Software com Java
Marcio Quirino - 88
o outro à direita do operador. Todo elemento de uma expressão, seja um número, seja um operador,
implementa a operação accept definida na superclasse ElementoExpressao.
public abstract class ElementoExpressao {
public abstract void accept(VisitorExpressaoAritmetica visitor);
}
public82
Padrão Observer ...................................................................................................................................... 82
Intenção do padrão Observer ................................................................................................................... 82
Problema resolvido pelo padrão Observer ........................................................................................... 82
Solução do padrão Observer ................................................................................................................ 83
Consequências e padrões relacionados ao Observer .......................................................................... 85
Padrão Visitor ........................................................................................................................................... 86
Intenção do padrão Visitor ....................................................................................................................... 86
Padrões de Projetos de Software com Java
Marcio Quirino - 4
Problema resolvido pelo padrão Visitor ................................................................................................ 86
Solução do padrão Visitor .................................................................................................................... 87
Consequências e padrões relacionados ao Visitor ............................................................................... 89
Padrão State ............................................................................................................................................ 90
Intenção do padrão State ..................................................................................................................... 90
Problema resolvido pelo padrão State ................................................................................................. 90
Solução do padrão State ...................................................................................................................... 91
Consequências e padrões relacionados ao State ................................................................................ 92
4. Padrões de projeto comportamentais Interpreter e Template Method ................................. 92
Padrão Interpreter .................................................................................................................................... 92
Intenção do padrão Interpreter ............................................................................................................. 92
Problema do padrão Interpreter ........................................................................................................... 92
Solução do padrão Interpreter .............................................................................................................. 92
Consequências e padrões relacionados ao Interpreter ........................................................................ 93
Padrão Template Method ......................................................................................................................... 94
Intenção do padrão Template Method .................................................................................................. 94
Problema do padrão Template Method ................................................................................................ 94
Solução do padrão Template Method .................................................................................................. 94
Consequências e padrões relacionados ao Template Method ............................................................. 95
Explore + .................................................................................................................................................. 96
Referências .............................................................................................................................................. 96
Padrões GRASP .................................................................................................................................. 97
Introdução ................................................................................................................................................ 97
1. Padrões Especialista na Informação e Criador ..................................................................... 97
O padrão Especialista na Informação ...................................................................................................... 97
Solução do Especialista na Informação................................................................................................ 98
Consequências do Especialista na Informação .................................................................................. 101
O padrão Criador .................................................................................................................................... 101
Solução do Criador ............................................................................................................................. 101
Consequências do Criador ................................................................................................................. 103
2. Padrões Coesão Alta e Controlador .................................................................................... 103
O padrão Coesão Alta ............................................................................................................................ 103
Solução da Coesão Alta ..................................................................................................................... 104
Coesão coincidente ............................................................................................................. 104
Coesão lógica ...................................................................................................................... 104
Coesão temporal ................................................................................................................. 105
Coesão procedural .............................................................................................................. 106
Coesão de comunicação ..................................................................................................... 106
Coesão sequencial .............................................................................................................. 107
Coesão funcional ................................................................................................................. 107
Padrões de Projetos de Software com Java
Marcio Quirino - 5
Consequências da Coesão Alta ......................................................................................................... 108
O padrão Controlador ............................................................................................................................. 108
Solução do Controlador ...................................................................................................................... 108
Consequências do Controlador .......................................................................................................... 109
3. Padrões Acoplamento Baixo e Polimorfismo ...................................................................... 110
O padrão Acoplamento Baixo................................................................................................................. 110
Solução do Acoplamento Baixo .......................................................................................................... 111
Acoplamento de conteúdo .................................................................................................. 111
Acoplamento global .............................................................................................................class NumeroInteiro extends ElementoExpressao {
private int valor;
public NumeroInteiro(int valor) {
this.valor = valor;
}
public void accept(VisitorExpressaoAritmetica visitor) {
visitor.visitNumeroInteiro(this);
}
public int getValor() {
return valor;
}
}
public abstract class OperadorAritmetico extends ElementoExpressao {
private ElementoExpressao operandoEsquerdo;
private ElementoExpressao operandoDireito;
public OperadorAritmetico(ElementoExpressao operandoEsquerdo, ElementoExpressao
operandoDireito) {
this.operandoEsquerdo = operandoEsquerdo;
this.operandoDireito = operandoDireito;
}
public abstract String getOperador();
public ElementoExpressao getOperandoEsquerdo() {
return operandoEsquerdo;
}
public void setOperandoEsquerdo(ElementoExpressao operandoEsquerdo) {
this.operandoEsquerdo = operandoEsquerdo;
}
public ElementoExpressao getOperandoDireito() {
return operandoDireito;
}
public void setOperandoDireito(ElementoExpressao operandoDireito) {
this.operandoDireito = operandoDireito;
}
}
public class OpSoma extends OperadorAritmetico {
public OpSoma(ElementoExpressao operandoEsquerdo, ElementoExpressao operandoDireito) {
super(operandoEsquerdo, operandoDireito);
}
public String getOperador() {
return '+';
}
public void accept(VisitorExpressaoAritmetica visitor) {
visitor.visitOpSoma(this);
}
}
A avaliação do resultado de uma expressão é definida em um Visitor separadamente da estrutura da
expressão. A interface VisitorExpressaoAritmetica define uma operação visitor para cada elemento da
estrutura, enquanto a classe VisitorCalculadora implementa as operações necessárias para calcular o valor
de uma expressão.
Note que a navegação pelos elementos da estrutura é definida no visitor. Por exemplo, a operação
soma precisa, em primeiro lugar, avaliar a expressão do operando à esquerda do operador, para depois
avaliar a expressão à direita, e finalmente gerar o valor da expressão, somando o resultado das duas
expressões.
public interface VisitorExpressaoAritmetica {
Padrões de Projetos de Software com Java
Marcio Quirino - 89
void visitNumeroInteiro(NumeroInteiro numero);
void visitOpSoma(OpSoma opSoma);
}
public class VisitorCalculadora implements VisitorExpressaoAritmetica {
private int valor;
public int getValor() {
return valor;
}
public void visitNumeroInteiro(NumeroInteiro numero) {
valor = numero.getValor();
}
public void visitOpSoma(OpSoma opSoma) {
valor = obterValorExpressao(opSoma.getOperandoEsquerdo()) +
obterValorExpressao(opSoma.getOperandoDireito());
}
private int obterValorExpressao(ElementoExpressao expressao) {
VisitorCalculadora visitor = new VisitorCalculadora();
expressao.accept(visitor);
return visitor.getValor();
}
}
public class TestCalculadora {
public static void main(String[] args) {
// definição da expressão 10 + 20 + 30
ElementoExpressao expressao =
new OpSoma(
new OpSoma(new NumeroInteiro(10), new NumeroInteiro(20)),
new NumeroInteiro(30));
VisitorCalculadora visitorCalc = new VisitorCalculadora();
expressao.accept(visitorCalc);
System.out.println('resultado = ' + visitorCalc.getValor());
}
}
Praticar é a melhor forma de fixar o conteúdo. Nesse sentido, recomendamos as seguintes
atividades:
• Implemente as operações de multiplicação, divisão e subtração, para o exemplo.
• Implemente outro visitor capaz de imprimir a expressão. Ex: 10 + 20
Consequências e padrões relacionados ao Visitor
O padrão Visitor permite a adição de novas funcionalidades de forma ortogonal a uma estrutura de
objetos. Como vimos no exemplo das expressões, diversas funcionalidades podem ser implementadas como
um visitor. Veja alguns exemplos:
• Cálculo
• Formatação
• Verificação sintática
• Verificação semântica da expressão
Desse modo, a estrutura original de objetos não fica poluída com operações não relacionadas entre
si. Entretanto, a adição de um novo elemento à estrutura de objetos afeta todos os visitors implementados,
pois será necessário adicionar uma operação de visita em cada uma dessas classes. Portanto, o padrão
Visitor não é adequado para estruturas que mudem com frequência.
Atenção
O padrão Visitor pode ser utilizado em conjunto com os padrões Composite e Interprete.
Padrões de Projetos de Software com Java
Marcio Quirino - 90
Padrão State
Diversos elementos assumem comportamentos diferentes de acordo com o estado corrente, como
no caso de uma porta automática, na qual o sensor indicando a presença de uma pessoa causaria a
abertura, e a ausência o fechamento da porta. Um exemplo na área de sistemas seria o download de
arquivos, no qual teríamos os estados requerido, iniciado, executando e concluído, com diferentes
informações sendo exibidas a cada instante, como o percentual baixado durante a execução. Por meio da
utilização do padrão State, seremos capazes de encapsular o tratamento dessas informações em classes
separadas para cada estado da operação, deixando nosso código mais limpo e organizado.
Intenção do padrão State
O padrão State permite que um objeto modifique o seu comportamento quando o seu estado mudar,
como se o objeto tivesse mudado de classe. Em vez de uma única classe tratar os estados dos seus objetos
em operações com diversas expressões condicionais, cada estado é representado em uma classe separada.
Problema resolvido pelo padrão State
O padrão State é muito útil para problemas envolvendo a implementação de entidades com uma
dinâmica de estados relevante. Por exemplo, suponha um sistema de ponto de venda de uma loja. Um ponto
de venda pode estar fechado, disponível para vendas ou com uma venda em curso. Eventos provocam a
transição entre esses estados. Por exemplo, no início do dia, o caixa está fechado e ao receber o evento
iniciar dia, ele passa para o estado disponível. Um mesmo evento pode levar para diferentes estados, como
é o caso do evento iniciar sangria, que pode ocorrer quando o PDV está no estado Disponível ou Vendendo.
O código a seguir ilustra uma implementação convencional sem a utilização do padrão. Note como
essa implementação é baseada em código condicional embasado no estado do PDV. Para uma máquina de
estados mais complexa, com mais estados e eventos, esse tipo de solução torna o código muito complexo,
difícil de testar e modificar.
public class PDV {
private int estado;
public iniciarSangria() {
if (estado == ESTADO_PDV.Disponivel) {
// tratamento da sangria quando o PDV está disponível
// estado = SangriaNormal;
} else if (estado == ESTADO_PDV.Vendendo) {
// tratamento da sangria quando o PDV está vendendo
// estado = SangriaExpressa;
} else {
// evento não deve ser aceito
}
}
Padrões de Projetos de Software com Java
Marcio Quirino - 91
Solução do padrão State
A estrutura da solução proposta pelo padrão State é apresentada no diagrama UML a seguir. O
participante Context corresponde a uma classe que possui uma dinâmica dependente de estados. Cada
classe concreta ConcreteState implementa o comportamento da classe Context associado a um estado
específico. A classe Context possui um atributo do tipo State, que correspondeao estado corrente do objeto.
Quando um objeto Context recebe uma requisição, a execução é passada para o estado corrente por meio
da operação handle definida na interface State.
O exemplo a seguir mostra como o PDV descrito no problema pode ser implementado com o padrão.
Essa é uma implementação parcial apenas para você entender a estrutura da solução. A classe PDV
corresponde ao participante Context. Ele possui um atributo do tipo PDV_Estado, que corresponde ao
participante State. PDV_Estado é uma interface que define todos os eventos que o contexto deve tratar.
Cada evento é definido como uma operação distinta.
As classes PDV_Estado_Disponivel e PDV_EstadoVendendo são dois exemplos de classes
concretas que correspondem a implementações distintas da interface genérica de eventos.
public class PDV {
private PDV_Estado estadoCorrente;
public void mudarEstado(PDV_Estado estado) {
this.estadoCorrente = estado;
}
public iniciarSangria() {
// contexto é passado como argumento para a operação do estadoCorrente
estadoCorrente.iniciarSangria(this);
}
}
public abstract class PDV_Estado {
public abstract void iniciarSangria(PDV pdv);
}
public class PDV_Estado_Disponivel() extends PDV_Estado {
public void iniciarSangria(PDV pdv) {
// inicia o processo de sangria normal
contexto.mudarEstado (new PDV_Estado_SangriaNormal());
}
}
public class PDV_Estado_Vendendo() extends PDV_Estado {
public void iniciarSangria (PDV pdv) {
// inicia o processo de sangria expressa
contexto.mudarEstado (new PDV_Estado_SangriaExpressa());
}
}
Padrões de Projetos de Software com Java
Marcio Quirino - 92
Consequências e padrões relacionados ao State
O padrão State separa os comportamentos aplicáveis a cada estado de um objeto em classes
distintas. Por outro lado, essa solução gera um número bem maior de classes, o que pode ser bom,
especialmente quando existirem muitos estados e muitas operações que dependam desses estados.
Esse padrão também melhora a compreensão do código, pois além de eliminar código condicional
extenso baseado no estado corrente, ele explicita as transições de estado.
Atenção
O padrão State pode ser combinado com o padrão Flyweight, permitindo o compartilhamento de objetos
estados por muitos objetos contextuais, evitando a criação de um objeto estado para cada objeto contexto.
4. Padrões de projeto comportamentais Interpreter e
Template Method
Padrão Interpreter
O padrão Interpreter, como seu nome sugere, define um interpretador de comandos, normalmente
baseado em um conjunto de regras sintáticas.
Um dos melhores exemplos para adoção do padrão Interpreter é na compilação de expressões
regulares, comuns na filtragem e identificação de conteúdo em sequências de texto. Outro exemplo seria os
dados fornecidos por satélites meteorológicos, que são codificados na forma de texto, segundo regras bem
definidas. O padrão Interpreter pode ser utilizado para controlar as regras de leitura desses dados e
consequente extração das informações, permitindo alimentar outros sistemas, como na visualização de
mapas meteorológicos.
Intenção do padrão Interpreter
O propósito do padrão Interpreter é definir uma representação para a gramática de uma linguagem
e um módulo capaz de interpretar sentenças nessa linguagem.
Problema do padrão Interpreter
Em sistemas que trabalham com cálculos customizáveis, uma solução comum consiste em definir
esses cálculos utilizando expressões matemáticas. Uma expressão matemática deve seguir uma gramática
que estabelece as regras de formação das expressões.
O problema resolvido pelo padrão Interpreter consiste em definir uma forma de representar e
interpretar uma linguagem definida por uma gramática.
Solução do padrão Interpreter
A estrutura da solução proposta pelo padrão Interpreter está representada no diagrama de classes a
seguir.
Padrões de Projetos de Software com Java
Marcio Quirino - 93
Os elementos da gramática da linguagem são definidos por objetos que formam uma árvore sintática
abstrata. Os símbolos terminais correspondem ao participante TerminalExpression, enquanto os elementos
compostos por outros correspondem ao participante NonTerminalExpression.
Veja, na imagem a seguir, como a expressão a + (10 * b) – (c / 5) pode ser representada por uma
estrutura hierárquica de objetos.
Atenção
Nessa gramática, constantes e variáveis são elementos terminais, enquanto os operadores de soma,
subtração, multiplicação e divisão são elementos não terminais, estando ligados a um elemento à esquerda e outro à
direita.
Consequências e padrões relacionados ao Interpreter
Esse padrão facilita a modificação e a ampliação de uma gramática. A ideia é implementar a estrutura
com uma classe simples para cada elemento da gramática, em vez de tentar centralizar a solução em uma
única classe. O padrão é:
A. Indicado
✓ Para linguagens com uma gramática simples, como é o caso, por exemplo, das expressões
aritméticas.
B. Não indicado
✓ Para linguagens mais complexas, recomenda-se utilizar outras soluções, como ferramentas
que gerem automaticamente interpretadores.
Padrões de Projetos de Software com Java
Marcio Quirino - 94
A operação interpret, definida nos participantes do padrão, pode ser vista como um processamento
específico que se deseja realizar com esses elementos. Por exemplo, no caso das expressões aritméticas,
o processamento poderia ser calcular o resultado. Entretanto, podem existir outros processamentos, como:
• Verificação sintática
• Verificação semântica
• Obtenção do texto da expressão
Nesses casos, recomenda-se aplicar o padrão Visitor em conjunto com o Interpreter, em que o
Interpreter define a estrutura dos elementos da linguagem, enquanto cada processamento é implementado
em uma classe Visitor específica.
Padrão Template Method
Hoje em dia é comum a utilização de frameworks na área de desenvolvimento, sendo definido um
modelo funcional padrão, que é personalizado pelo desenvolvedor para as regras de negócio do aplicativo.
Por meio do padrão Template Method, podemos definir macroprocessos com lacunas especificadas em
métodos abstratos, permitindo ao programador completar a funcionalidade exigida por meio de polimorfismo,
como ocorre nos frameworks. Um exemplo prático seria a implementação do algoritmo HMAC, que exige
uma cifra assimétrica e um algoritmo de hash, utilizados em uma sequência de chamadas bem definida,
permitindo que um desenvolvedor trabalhe com RSA e SHA-3, enquanto outro utiliza ECDSA e SHA-2.
Intenção do padrão Template Method
O propósito do padrão Template Method é definir o esqueleto de um algoritmo em uma superclasse,
em que os passos comuns podem ser implementados na própria superclasse e os passos específicos são
implementados nas subclasses.
Problema do padrão Template Method
Suponha que você esteja implementando um sistema que deve gerar alguns relatórios textuais. A
preparação dos dados para qualquer relatório segue três passos fundamentais: gerar o cabeçalho, gerar o
corpo e gerar o sumário.
Imagine, por exemplo, que você tenha que implementar um relatório de vendas e um relatório de
devoluções em um período. O código a seguir ilustra uma solução inicial para esses relatórios.
public class ServicoRelatorio {
public void gerarRelatorioVendas (Date dataInicial, Date dataFinal) {
// código para gerar cabeçalho do relatório de vendas
// código para gerar corpo do relatório de vendas
// código para gerar sumário do relatório de vendas
}
public void gerarRelatorioDevolucoes (Date dataInicial, Date dataFinal) {
// código para gerar cabeçalho do relatóriode devoluções
// código para gerar corpo do relatório de devoluções
// código para gerar sumário do relatório de devoluções
}
}
Essa solução não é adequada, primeiro por ser baseada em repetição de código com estrutura muito
similar, apesar de diferirem nos detalhes de geração de cada relatório. Segundo, para cada novo relatório,
é necessário abrir a classe ServicoRelatorio e acrescentar o código específico desse relatório,
provavelmente copiando, colando e adaptando o código de um dos relatórios já implementados.
Solução do padrão Template Method
A estrutura da solução proposta pelo padrão TemplateMethod está representada no diagrama de
classes a seguir:
Padrões de Projetos de Software com Java
Marcio Quirino - 95
O padrão sugere definir um método em uma classe abstrata que implemente a estrutura do algoritmo
comum a todas as suas implementações específicas. A estrutura desse algoritmo é composta por um
conjunto de passos que podem ser redefinidos pelas subclasses. Cada ponto de variação no algoritmo
corresponde à definição de uma operação na superclasse que pode ser especializada nos seus
descendentes. Esses pontos de variação podem ser:
1. Operações abstratas
✓ Tais operações obrigarão as subclasses a implementá-las.
2. Métodos hook (gancho)
✓ Operações realizadas na superclasse e que podem ser substituídas por implementações
específicas em uma ou mais subclasses.
O código a seguir ilustra a estrutura de implementação para os relatórios com a aplicação desse
padrão. A classe abstrata Relatorio define três operações abstratas que serão implementadas por todas as
subclasses. A operação gerar é o método padrão (template method) que define a estrutura comum do
algoritmo, seguida por todos os relatórios. Desse modo, cada relatório define as particularidades da geração
de cabeçalho, corpo e sumário na implementação dessas operações genéricas.
public abstract class Relatorio {
public abstract void gerarCabecalho();
public abstract void gerarCorpo();
public abstract void gerarSumario();
public void gerar() { // template method
gerarCabecalho();
gerarCorpo();
gerarSumario();
}
}
public class RelatorioVendas extends Relatorio {
public void gerarCabecalho() {
// código para gerar o cabeçalho do relatório de vendas
}
public void gerarCorpo() {
// código para gerar o corpo do relatório de vendas
}
public void gerarSumario() {
// código para gerar o sumário do relatório de vendas
}
}
Outro benefício dessa implementação é que o código em comum entre os diferentes relatórios pode
ser efetuado uma única vez na superclasse Relatório, reduzindo a quantidade de código repetido.
Consequências e padrões relacionados ao Template Method
O padrão Template Method é muito utilizado na implementação de bibliotecas genéricas de classes
e de frameworks. O resultado obtido pela sua aplicação é conhecido como o princípio de Hollywood, isto é,
Padrões de Projetos de Software com Java
Marcio Quirino - 96
“Não nos chame, nós o chamaremos”, uma referência a como a superclasse chama as operações
específicas das subclasses e não o contrário.
O padrão Factory Method costuma ser utilizado no contexto de um Template Method, nos casos em
que um dos passos do algoritmo seja instanciar algum objeto específico. Os padrões Template Method e
Strategy são formas de representação de algoritmos:
1. Template Method
✓ Permite a variação de partes de um algoritmo, por meio de uma estrutura de herança.
2. Strategy
✓ É aplicado quando desejamos criar algoritmos diferentes, mas que não possuam uma
estrutura comum.
Explore +
Para saber mais sobre a programação orientada a objetos, acesse o site da DevMedia e leia o artigo
Utilização dos princípios SOLID na aplicação de padrões de projeto.
O site Padrões de Projeto / Design patterns – Refactoring.Guru apresenta um conteúdo interativo e
bastante completo de todos os padrões GoF, com exemplos de código em diversas linguagens de
programação.
Além dos padrões GoF tradicionais, outros padrões voltados para o desenvolvimento de aplicações
corporativas em Java EE podem ser encontrados no livro Java EE 8 Design Patterns and Best Practices, de
Rhuan Rocha e João Purificação, de 2018. A obra aborda padrões para interface com o usuário, lógica do
negócio, integração de sistemas, orientação a aspectos, programação reativa e microsserviços.
Referências
GAMMA, E.; HELM, R.; JOHNSON, R.; VLISSIDES, J. Design Patterns: Elements of Reusable
Object-Oriented Software. 1. ed. Boston: Addison-Wesley, 1994.
METSKER, S. J.; WAKE, W. C. Design Patterns in Java. 1. ed. Boston: Addison-Wesley, 2006.
Padrões de Projetos de Software com Java
Marcio Quirino - 97
Padrões GRASP
Introdução
GRASP é o acrônimo para o termo em inglês general responsibility assignment software patterns
proposto por Craig Larman no livro Applying UML and patterns, que define as diretrizes para a atribuição de
responsabilidades em software. Os padrões GRASP podem ser vistos como os princípios gerais de um
projeto de software orientado a objetos que são aplicáveis na solução de diversos problemas específicos.
A distribuição de responsabilidades pelos módulos do sistema é uma das tarefas mais importantes
no desenvolvimento orientado a objetos. Diagramas UML são veículos que nos permitem expressar e discutir
decisões sobre as responsabilidades de cada módulo.
No entanto, as decisões tomadas é que realmente são importantes. Com isso, seguir padrões e
princípios bem estabelecidos aumenta nossas chances de tomar decisões que resultem em um software
mais fácil de ser evoluído ao longo da sua existência. Neste conteúdo, você vai conhecer os padrões GRASP
e entender os problemas de projeto de software que eles resolvem.
1. Padrões Especialista na Informação e Criador
O padrão Especialista na Informação
No desenvolvimento de um sistema orientado a objetos, é comum elaborar um modelo UML
representando os principais conceitos e dados no escopo do sistema por meio de classes, atributos e
associações. Entretanto, quando se elabora a solução técnica de um projeto, as principais questões a serem
respondidas passam a ser:
Quais responsabilidades cada classe deve possuir?
Quais serão as interações necessárias entre os objetos dessas classes para o sistema realizar as
funcionalidades esperadas?
Suponha que você esteja desenvolvendo um sistema de venda de produtos pela internet. A figura 1
apresenta um modelo simplificado de classes desse domínio.
Digamos que o sistema deva listar os itens e o valor total do pedido do cliente, sendo que esse valor
é uma informação calculada a partir dos produtos e das suas respectivas quantidades compradas. Como
você organizaria as responsabilidades entre as classes para fornecer essa informação?
Padrões de Projetos de Software com Java
Marcio Quirino - 98
Solução do Especialista na Informação
Este padrão recomenda como princípio geral a atribuição correta de responsabilidade. Você pode
estar se perguntando: mas o que isso significa?
Atribua a responsabilidade ao especialista, isto é, ao módulo que possua o conhecimento
necessário para realizá-la.
Atribuir a responsabilidade ao especialista é uma heurística intuitiva que utilizamos no nosso
cotidiano. Suponha que você precise trocar o encanamento da sua casa. A quem você atribuiria essa
responsabilidade? Provavelmente você recorrerá a um especialista para realizar tal atividade.
Atenção
A transposição dessa heurística do nosso cotidiano para o mundo de objetos em software é conhecida por
heurística antropomórfica, ou seja, imaginamos os objetos como pessoas que possuem determinado conhecimento e
que podem utilizá-lo para realizar algumas tarefas. Comoprojetista de software, você assume o papel de diretor desse
universo de objetos, podendo definir quantas classes quiser – cada qual com as responsabilidades que você achar
melhor.
Voltemos para o nosso problema do sistema de venda de produtos: o valor total do pedido pode ser
definido como a soma do valor de cada um de seus itens. Segundo o padrão Especialista na Informação, a
responsabilidade tem de ficar com o detentor da informação.
Nesse caso, quem conhece todos os itens que compõem um pedido? O próprio pedido, não é
mesmo? Então que tal definir uma operação obterValorTotal na classe Pedido? Mas onde ficaria o cálculo
do preço de cada item do pedido? Na própria classe Pedido? Pensemos um pouco mais nessa questão:
1. Quais informações são necessárias para esse cálculo?
✓ A quantidade e o preço do produto, já que o valor de um item é a multiplicação desses dois
valores.
2. Quem conhece essas informações?
✓ A classe ItemPedido conhece a quantidade e o produto associado.
Vamos definir, portanto, uma operação obterValor na classe ItemPedido. E o preço do produto? Basta
o objeto item do pedido solicitar essa informação ao produto (acessível pelo relacionamento entre eles) por
meio da operação de acesso getPrecoUnitario.
O código a seguir apresenta a implementação das classes Pedido, ItemPedido e Produto,
considerando essa distribuição de responsabilidades:
public class Pedido {
private int numero;
private Date data;
private List itens;
public BigDecimal obterValorTotal() {
BigDecimal resultado = new BigDecimal(0);
itens.forEach(item -> {
resultado.add(item.obterValor());
});
return resultado;
}
public int getNumero() {
return numero;
}
public void setNumero(int numero) {
this.numero = numero;
}
public Date getData() {
return data;
}
public void setData(Date data) {
Padrões de Projetos de Software com Java
Marcio Quirino - 99
this.data = data;
}
public Iterator getItens() {
return itens.iterator();
}
public void adicionarItem(int quantidade, Produto produto) {
itens.add(new ItemPedido(itens.size() + 1, quantidade, produto));
}
}
public class ItemPedido {
private int numero;
private int quantidade;
private Produto;
public ItemPedido(int numero, int quantidade, Produto) {
this.numero = numero;
this.quantidade = quantidade;
this.produto = produto;
}
public BigDecimal obterValor() {
return produto.getPrecoUnitario().multiply(new BigDecimal(quantidade));
}
public int getNumero() {
return numero;
}
public void setNumero(int numero) {
this.numero = numero;
}
public int getQuantidade() {
return quantidade;
}
public void setQuantidade(int quantidade) {
this.quantidade = quantidade;
}
public Produto getProduto() {
return produto;
}
public void setProduto(Produto produto) {
this.produto = produto;
}
}
public class Produto {
private String codigo;
private String descricao;
private BigDecimal precoUnitario;
private File imagem;
public String getCodigo() {
return codigo;
}
public void setCodigo(String codigo) {
this.codigo = codigo;
}
public String getDescricao() {
return descricao;
}
public void setDescricao(String descricao) {
this.descricao = descricao;
}
public BigDecimal getPrecoUnitario() {
return precoUnitario;
}
public void setPrecoUnitario(BigDecimal precoUnitario) {
this.precoUnitario = precoUnitario;
}
public File getImagem() {
return imagem;
Padrões de Projetos de Software com Java
Marcio Quirino - 100
}
public void setImagem(File imagem) {
this.imagem = imagem;
}
}
Classes Pedido, ItemPedido e Produto.
O diagrama de sequência modela a colaboração entre os objetos que implementam o cálculo do
valor total de um pedido, como exemplificada na figura 2.
Uma alternativa para a alocação da responsabilidade pelo cálculo do valor de um item do pedido
seria considerar a classe Produto como a especialista em preço. No entanto, ela precisa de uma informação
adicional (a quantidade) para cumprir essa responsabilidade.
Desse modo, podemos definir a operação obterValorParaQuantidade na classe Produto, fazendo
com que a operação obterValor (definida em ItemPedido) passe a quantidade como argumento. Nesse caso,
o cálculo do valor para determinada quantidade fica na classe Produto, que pode eventualmente aplicar
políticas de desconto específicas conforme a quantidade e o tipo do produto, por exemplo. O código a seguir
apresenta a implementação dessa alternativa de alocação de responsabilidades:
public class ItemPedido {
public ItemPedido(int numero, int quantidade, Produto produto) {
this.numero = numero;
this.quantidade = quantidade;
this.produto = produto;
}
public BigDecimal obterValor() {
return produto.obterPrecoParaQuantidade(quantidade);
}
… // restante da classe ItemPedido
}
public class Produto {
private String codigo;
private String descricao;
private BigDecimal precoUnitario;
private File imagem;
public BigDecimal obterPrecoParaQuantidade(int quantidade) {
return precoUnitario.multiply(quantidade);
}
// restante da classe Produto
}
Padrões de Projetos de Software com Java
Marcio Quirino - 101
Consequências do Especialista na Informação
Normalmente, a realização de uma funcionalidade do sistema envolve a presença de diversos
especialistas, pois cada classe possui uma parte das informações necessárias para resolver o problema.
Dessa forma, será necessário estabelecer um mecanismo de colaboração entre os objetos – por intermédio
da troca de mensagens – para realizar a função maior.
Quando o padrão Especialista na Informação não é seguido, é comum encontrar uma solução
deficiente conhecida como God Class. Essa solução consiste em duas etapas:
1. Etapa 1
✓ Definir, nas classes de domínio, apenas operações de acesso aos seus atributos (operações
conhecidas como getters e setters).
2. Etapa 2
✓ Concentrar a lógica de determinada funcionalidade do sistema em uma única classe
(usualmente definida na forma de uma classe de controle ou de um serviço) na qual se
encontram algoritmos complexos utilizando as operações de acesso das diversas classes de
domínio, as quais, nesse estilo de solução, são meras fornecedoras de dados.
Há, porém, situações em que a utilização desse padrão pode comprometer conceitos mais
fundamentais, como, por exemplo, a coesão e o acoplamento. Uma delas ocorre quando existe algum
aspecto tecnológico envolvido, como é o caso do armazenamento de dados ou da interface com usuário.
Qual classe deveria ser responsável por implementar o armazenamento dos dados de um objeto da
classe Pedido no banco de dados?
Resposta
Pelo padrão Especialista na Informação, deveria ser a própria classe Pedido, uma vezque ela possui as
informações que serão armazenadas. Contudo, se implementarmos a solução de armazenamento na própria classe
Pedido, a classe de negócio ficará acoplada com conceitos relativos à tecnologia de armazenamento (exemplos: SQL,
NoSQL e arquivos). Isso fere o princípio fundamental da coesão, pois a classe Pedido ficaria sujeita a duas fontes de
mudança: mudanças no negócio e na tecnologia de armazenamento utilizada, o que é claramente inadequado.
O padrão Criador
A instanciação de objetos é uma das instruções mais presentes em um programa orientado a objetos.
Sempre que necessário, pode-se utilizar o operador new para criar um objeto em Java. Entretanto, a
instanciação indiscriminada – e sem critérios bem definidos – de objetos em diferentes partes do código
tende a gerar uma estrutura pouco flexível, difícil de modificar e com alto acoplamento entre os módulos.
Quando se cria um objeto da classe B em um método da classe A por meio de um comando
“new B()”, estabelece-se uma relação de dependência entre duas implementações.
A dependência acontece porque, em Java, uma classe é uma implementação concreta de um
conjunto de operações. Nesse exemplo, portanto, A é dependente de B.
Em várias situações, entretanto, a criação de uma relação de dependência entre duas ou mais
classes torna o sistema inflexível, dificultando a sua evolução. Deve-se estruturar um projeto para fazer com
que as implementações dependam de abstrações, especialmente em casos nos quais um módulo depende
de um serviço que pode ter diferentes implementações. Dessa forma, a pergunta que esse padrão tenta
responder é: quem deve ser responsável pela instanciação de um objeto de determinada classe?
Solução do Criador
O padrão Criador recomenda atribuir a uma classe X a responsabilidade de criar uma instância da
classe Y se uma ou mais das seguintes condições for(em) verdadeira(s):
• X é um agregado formado por instâncias de Y.
Padrões de Projetos de Software com Java
Marcio Quirino - 102
• X contém instâncias de Y.
• X registra instâncias de Y.
• X possui os dados de inicialização necessários para a criação de uma instância de Y.
No exemplo do sistema de vendas de produtos pela internet, considerando o modelo de classes
ilustrado na figura 3, qual classe deveria ser a responsável por criar uma instância da classe ItemPedido?
Uma abordagem comum – mas inadequada – é instanciar esse item em uma classe de serviço e
apenas acumulá-lo no Pedido. Entretanto, quando se trata de um agregado, isto é, um objeto composto por
vários itens, a responsabilidade pela criação dos itens, segundo o padrão Criador, deve ser alocada ao
agregado, que é responsável por todo o ciclo de vida dos seus itens (criação e destruição).
Veja no código adiante que a classe Pedido contém uma lista de itens. Essa lista implementa o
relacionamento de composição entre Pedido e ItemPedido, só podendo ser modificada dentro da classe
Pedido, pois ela controla o ciclo de vida dos seus itens. A criação de um novo item do pedido é realizada
pela operação adicionarItem que recebe os parâmetros quantidade e produto.
public class Pedido {
private int numero;
private java.util.Date data;
private List itens;
public void adicionarItem(int quantidade, Produto produto) {
itens.add(new ItemPedido(itens.size() + 1, quantidade, produto));
}
public Iterator getItens() {
return itens.iterator();
}
public BigDecimal obterValorTotal() {
BigDecimal resultado = new BigDecimal(0);
itens.forEach(item -> {
resultado.add(item.obterValor());
});
return resultado;
}
public int getNumero() {
return numero;
}
public void setNumero(int numero) {
this.numero = numero;
}
public Date getData() {
return data;
}
public void setData(Date data) {
this.data = data;
}
}
Padrões de Projetos de Software com Java
Marcio Quirino - 103
O diagrama ilustrado na figura 4 apresenta a interação entre os objetos nessa solução. Um objeto de
serviço, por exemplo, solicita ao objeto Pedido que adicione um novo item correspondente ao produto e à
quantidade de itens passados como argumentos da mensagem adicionarItem. O objeto Pedido, por sua vez,
utiliza esses argumentos para instanciar um novo ItemPedido, acrescentando-o a uma lista de itens que
implementa o relacionamento entre o pedido e os seus itens.
Consequências do Criador
O padrão Criador é especialmente indicado para a criação de instâncias que formam parte de um
agregado, pois o elemento que controla o ciclo de vida das suas partes é o próprio agregado, o qual, aliás,
naturalmente já está relacionado com as suas partes. Esse padrão não é apropriado em algumas situações
especiais, como é o caso da criação condicional de uma instância de uma família de classes similares.
Exemplo
Um sistema de vendas trabalha com diferentes soluções de pagamento. Por questões contratuais, a loja A
opera com a solução de pagamento X; e a B, com a solução de pagamento Y. O problema passa a ser como criar a
instância em conformidade com alguma parametrização externa feita para cada loja. Nesse caso, deve-se delegar a
instanciação para uma classe auxiliar denominada Fábrica, aplicando o padrão de projeto GoF Abstract Factory.
De forma geral, quando a instanciação de objetos envolver cenários mais complexos, como o
compartilhamento de objetos para racionalizar o uso de memória ou a criação de uma instância de uma
família de classes similares condicionada ao valor de alguma configuração externa, será mais adequado
aplicar padrões de projeto específicos, como os padrões GoF Abstract Factory, Builder, Prototype ou Factory
Method.
2. Padrões Coesão Alta e Controlador
O padrão Coesão Alta
Coesão é um conceito que nos permite avaliar se as responsabilidades de um módulo estão
fortemente relacionadas e possuem o mesmo propósito. O objetivo é criar módulos com coesão alta, ou
seja, módulos que tenham um propósito bem definido.
Módulos ou classes com coesão baixa realizam muitas operações pouco correlacionadas, gerando
sistemas de difícil entendimento, reuso e manutenção, além de muito mais vulneráveis às mudanças.
Portanto, a pergunta que esse padrão tenta responder é a seguinte: como definir as responsabilidades dos
módulos de forma que a complexidade do sistema resultante seja gerenciável, facilitando o seu
entendimento e futuras evoluções?
Padrões de Projetos de Software com Java
Marcio Quirino - 104
Solução da Coesão Alta
A solução proposta por esse padrão consiste em definir módulos de coesão alta.
Como se mede a coesão de um módulo?
O conceito de coesão está ligado ao critério utilizado para reunir um conjunto de elementos em um
mesmo módulo. Note que esse conceito pode ser aplicado a módulos de diferentes níveis de granularidade:
A. Coesão de um método de uma classe
✓ Um método reúne um conjunto de instruções. Pode-se avaliar se essas instruções formam
um método com um propósito bem definido.
B. Coesão de uma classe
✓ Uma classe reúne um conjunto de atributos e operações. Pode-se avaliar se esses atributos
e essas operações formam uma classe com um propósito bem definido.
C. Coesão de um pacote
✓ Um pacote reúne um conjunto de classes e interfaces. Pode-se avaliar se essas classes
formam um pacote com um propósito bem definido.
D. Coesão de um subsistema
✓ Um subsistema reúne um conjunto de pacotes.
A coesão de um módulo, seja ele uma classe, um pacote ou um subsistema, pode ser classificada
de acordo com o critério utilizado para reunir o conjunto dos elementos que o compõem. Vamos conheceragora esses critérios – do nível mais baixo para o mais alto de coesão.
Coesão coincidente
Quando os elementos estão agrupados em um módulo de forma arbitrária ou por conveniência,
dizemos que esse módulo possui coesão coincidente. De forma esquemática, o exemplo mais à frente
apresenta a classe Utils, que reúne operações, como a formatação de números, a conversão de medidas e
o envio de arquivos via FTP. Esse módulo possui coesão baixa, pois muitas responsabilidades de diferentes
naturezas estão reunidas em um único módulo.
public class Utils {
public String formatarData (Date data) {
// implementação da formatação de data no formato DD/MM/AAAA
}
public double converterMetrosEmPolegadas (double metros) {
// implementação da conversão de metros em polegadas
}
public double converterPolegadasEmMetros (double polegadas) {
// implementação da conversão de polegadas em metros
}
public void enviarArquivoPorFTP (File arquivo, String endereço) {
// implementação do envio do arquivo por FTP
}
}
Atenção
O que normalmente motiva a criação desse tipo de módulo é a conveniência para os desenvolvedores de um
projeto. O problema é que, se você quiser utilizar apenas uma dessas operações em um outro projeto, terá de trazer o
módulo completo, incluindo todas as operações não utilizadas. Além disso, qualquer modificação feita em uma
operação fará com que todo o módulo tenha de passar pelo ciclo de aprovação e de liberação.
Coesão lógica
Em um módulo com coesão lógica, os elementos são agrupados por estarem logicamente
relacionados ou por realizarem funções diferentes, ainda que tendo a mesma natureza. O código adiante
apresenta a estrutura de implementação de uma classe que reúne operações de leitura de dados de
produtos a partir de:
Padrões de Projetos de Software com Java
Marcio Quirino - 105
• Um arquivo texto local.
• Um arquivo obtido via FTP.
• Um banco de dados relacional.
Esse é um exemplo de classe com coesão lógica, pois são reunidas em uma mesma classe todas
as operações capazes de ler dados de produtos independentemente da fonte de dados disponível. Uma
solução mais adequada seria separar a leitura de cada fonte em um módulo à parte.
public class ProdutoRepository {
public static final int ARQUIVO_LOCAL = 1;
public static final int ARQUIVO_FTP = 2;
public static final int BANCO_RELACIONAL = 3;
public Produto obterProduto (int origem, String chave) {
switch (origem) {
case ARQUIVO_LOCAL:
return obterProdutoViaArquivoLocal (chave);
case ARQUIVO_FTP:
return obterProdutoViaArquivoFTP (chave);
case BANCO_RELACIONAL:
return obterProdutoViaBancoRelacional (chave);
}
}
private Produto obterProdutoViaArquivoLocal (String chave) {
// implementação da operação de leitura do Produto via arquivo local
}
private Produto obterProdutoViaArquivoFTP (String chave) {
// implementação da operação de leitura do Produto via arquivo FTP
}
private Produto obterProdutoViaBancoRelacional (String chave) {
// implementação da operação de leitura do Produto via banco relacional
}
}
Coesão temporal
Um módulo com coesão temporal é aquele em que os seus elementos são agrupados por serem
executados em determinado instante do tempo.
São colocadas no módulo Startup todas as operações executadas na inicialização do sistema, como,
por exemplo:
• Inicialização do log do sistema
• Inicialização da interface gráfica com o usuário
• Inicialização das conexões com banco de dados
• Inicialização das tarefas de segundo plano
Reúnem-se em um módulo, assim, as responsabilidades de diferentes naturezas por elas serem
executadas em um momento específico, isto é, a inicialização do sistema. Uma solução mais adequada
seria separar cada problema de inicialização (log, interface gráfica, banco de dados e assim por diante) em
um módulo à parte. O código adiante ilustra um exemplo de módulo com coesão temporal.
public class Startup {
public void inicializar() {
inicializarLog();
inicializarGUI();
inicializarConexoesBD();
inicializarTarefasSegundoPlano();
}
private void inicializarLog() {
// inicializar log do sistema
}
private void inicializarGUI() {
// inicializar interface gráfica com o usuário
}
Padrões de Projetos de Software com Java
Marcio Quirino - 106
private void inicializarConexoesBD() {
// inicializar conexões com banco de dados
}
private void inicializarTarefasSegundoPlano() {
// inicializar tarefas de segundo plano, executadas em threads específicas
}
}
Coesão procedural
Um módulo com coesão procedural é aquele cujos elementos são agrupados por eles serem
executados em determinada sequência utilizando diferentes conjuntos de dados. Normalmente, uma classe
com essa coesão corresponde à implementação de uma God Class, ou seja, ela recebe uma requisição de
execução de um serviço da aplicação e concentra nela própria o código de processamento dessa requisição
em vez de dividir essa responsabilidade em operações menores de outras classes.
O código à frente apresenta um exemplo da estrutura de um módulo com coesão procedural.
public class ServicoPedido {
public void confirmarPedido() {
obterDadosPagamento();
validarDadosPagamento();
aprovarPagamento();
salvarPedido();
atualizarEstoque();
enviarPedidoParaProvisionamento();
enviarEmailConfirmacao();
}
private void obterDadosPagamento() {
// obter dados do cliente para o pagamento
}
private void validarDadosPagamento() {
// validar os dados para o pagamento
}
private void aprovarPagamento() {
// realizar o pagamento na forma escolhida
}
private void salvarPedido() {
// salvar o pedido no banco de dados
}
private void atualizarEstoque() {
// atualizar o estoque, reservando os itens do pedido
}
private void enviarPedidoParaProvisionamento() {
// enviar o pedido para o setor de logística para provisionamento
}
private void enviarEmailConfirmacao() {
// enviar email de confirmação do pedido para o cliente
}
}
Nesse exemplo, a classe ServicoPedido reúne a implementação de todos os passos necessários
para a realização do procedimento de confirmação do pedido, como, entre outros passos, a obtenção dos
dados para pagamento, a aprovação do pagamento e o armazenamento do pedido no banco de dados.
Atenção
Perceba que a classe ServicoPedido trata de assuntos completamente diferentes – ainda que necessários –
para a confirmação do pedido. Esse estilo de construção deve ser evitado, pois dá origem a módulos de complexidade
muito alta e com pouca flexibilidade.
Coesão de comunicação
Em um módulo com coesão de comunicação, os elementos são agrupados por eles realizarem
funções diferentes utilizando o mesmo conjunto de dados. O código a seguir ilustra o esquema de um módulo
com essa coesão.
Padrões de Projetos de Software com Java
Marcio Quirino - 107
public class Conta {
public BigDecimal saldo() {
// obter o saldo da conta
}
public void enviarExtratoParaCliente(){
// gerar relatório com extrato da conta e enviar por e-mail para o cliente
}
public void salvar() {
// salvar os dados do cliente no banco de dados
}
}
Observe que a classe Conta reúne operações de naturezas diversas, como obtenção do saldo,
geração de extrato com envio para o cliente e armazenamento em banco de dados. O que elas têm em
comum é o fato de trabalharem sobre o mesmo conjunto de dados, isto é, os dados da conta.
Coesão sequencial
Um módulo com coesão sequencial é aquele cujos elementos são agrupados por eles conterem
todos os passos de execução de um procedimento sobre um mesmo conjunto original de dados, sendo que
os resultados de um passo são utilizados como a entrada para o passo seguinte. A responsabilidade aqui é
mais específica que a observada na coesão procedural, pois reúnem-se elementos que trabalham sobre o
mesmo conjunto de dados, formando um pipeline de processamento.
O código adiante contém o esquema de um módulo com coesão sequencial, pois o procedimento
transforma um bilhete recebido em formato string em um registro desse bilhete armazenado no banco de
dados.
public class ServicoBilhetagem {
public void capturarBilhete(String dados) {
Bilhete bilheteFormatado = formatarBilhete(dados);
Bilhete bilheteIdentificado = identificarBilhete(bilheteFormatado);
salvarBilhete(bilheteIdentificado);
}
private Bilhete formatarBilhete(String dados) {
// formata o bilhete, extraindo os campos do texto recebido como parâmetro.
}
private Bilhete identificarBilhete(Bilhete bilhete) {
// gera o campo de identificação do bilhete baseado nos seus campos
}
private void salvarBilhete(Bilhete bilhete) {
// salvar os dados do cliente no banco de dados
}
}
Nesse exemplo, o procedimento é formado por três passos, sendo que o resultado de um passo é
utilizado como a entrada para o passo seguinte. Além disso, os passos podem envolver o processamento
de naturezas diversas.
Coesão funcional
Um módulo com coesão funcional reúne elementos que, juntos, cumprem um único propósito bem
definido. As classes do pacote java.io da linguagem Java, por exemplo, estão nesse pacote pelo propósito
de elas reunirem todas as responsabilidades de entrada e de saída.
Nesse pacote, pode-se encontrar classes com responsabilidades bem específicas. Entre muitas
outras, destacam-se:
A. FileOutputStream
✓ Para a escrita de arquivos binários.
B. FileInputStream
✓ Para a leitura desses arquivos.
C. FileReader
✓ Para a leitura de arquivos texto.
Padrões de Projetos de Software com Java
Marcio Quirino - 108
D. FileWriter
✓ Para a escrita desses arquivos.
Dessa forma, o padrão Coesão Alta propõe a construção de módulos com coesão funcional. Cada
módulo, portanto, deve reunir elementos que contribuam para que um único propósito bem definido seja
atingido.
Consequências da Coesão Alta
Coesão é um dos princípios fundamentais em projetos de software. A base da modularidade de um
software está na definição de módulos com coesão alta e acoplamento baixo. Sistemas construídos com
módulos que apresentem uma coesão alta tendem a:
• Ser mais flexíveis.
• Ser mais fáceis de se entender e de se evoluir.
• Proporcionar maiores possibilidades de reutilização.
• Facilitar a elaboração de soluções de acoplamento baixo (enquanto a tendência dos módulos
de coesão baixa seja a de gerar soluções de acoplamento alto).
No que tange à complexidade, os sistemas com coesão alta diferem dos sistemas com coesão baixa
conforme vemos a seguir:
1. Coesão alta
✓ A complexidade está distribuída por vários módulos, cada um contribuindo para resolver um
pedaço específico do problema.
2. Coesão baixa
✓ A complexidade fica centralizada em um pequeno número de módulos, os quais, muitas
vezes, possuem centenas ou milhares de linhas de código.
O padrão Controlador
Um sistema interage com elementos externos, também conhecidos como atores. Muitos desses
elementos geram eventos que devem ser capturados e processados, e gerar alguma resposta, seja ela
interna ou externa.
Exemplo
Quando o cliente solicita o fechamento de um pedido em uma loja on-line, esse evento precisa ser capturado
e processado pelo sistema de vendas dela.
A quem devemos atribuir a responsabilidade de processar eventos que correspondam a
requisições lógicas de execução de operações do sistema?
Essa é a pergunta que o padrão Controlador tenta responder.
Solução do Controlador
O padrão Controlador recomenda que a responsabilidade de receber um evento de sistema e de
coordenar a produção da sua resposta precisa ser alocada a uma classe que represente uma das seguintes
opções:
1. Opção 1
✓ Uma classe correspondente ao sistema ou a um subsistema específico, solução também
conhecida pelo nome Controlador Fachada. Essa solução é normalmente utilizada em
sistemas com poucos eventos.
2. Opção 2
✓ Uma classe correspondente ao caso de uso em que o evento ocorre. Nesse caso, essa classe
pode ter o seu nome formado pelo nome do caso de uso e por um prefixo, como Processador,
Padrões de Projetos de Software com Java
Marcio Quirino - 109
Controlador, Serviço ou algo similar. Ela ainda deve reunir o tratamento de todos os eventos
que o sistema receba no contexto desse caso de uso. Tal solução é indicada como alternativa
a concentrar as responsabilidades de tratamento de eventos de diferentes naturezas em um
único Controlador Fachada, evitando, assim, a criação de um controlador com coesão baixa.
Um componente de interface captura os eventos de interface oriundos de teclado e mouse, por
exemplo, e gera uma requisição para o controlador, que é o primeiro objeto, depois da camada de interface
com o usuário, responsável por receber uma solicitação de sistema e coordenar a produção da respectiva
resposta.
O controlador não faz parte da interface com o usuário.
Em uma implementação Java Desktop, as classes do Java Swing geram as solicitações, enquanto,
em uma implementação com JSP, os servlets é que as geram. Em uma aplicação web rich client, por sua
vez, o código Javascript da interface com o usuário gera as solicitações para o controlador, o qual, portanto,
fornece uma interface de alto nível para as diferentes formas de interação do usuário com o sistema.
Exemplo
Em um sistema de internet banking, o usuário informa todos os dados de uma transferência de valores para
uma conta destino; ao pressionar o botão “transferir”, o componente de interface com o usuário gera uma requisição
para o controlador realizar o processamento lógico da transferência. Com isso, um mesmo controlador pode atender
às solicitações realizadas por diferentes interfaces com o usuário (web, dispositivo móvel, totem 24 horas etc.).
Veja no diagrama representado na figura 5 que, quando o usuário clica no botão “transferir”, após ter
preenchido os dados da transferência, o componente UI (interface com o usuário) gera uma requisição para
o controlador ServicoTransferencia, o qual, por sua vez, coordena a execução da transação interagindo com
os objetos Conta e ContaRepository.
Atenção
O componente UI não realiza nenhuma operação ligada à lógica do negócio nem precisa conhecer os diversos
componentes que compõem essa lógica. Ele, na verdade, apenas gera uma requisição para a classe controladora
solicitando a execução da operação de sistema correspondente às interações realizadas pelo usuário com os
componentes visuais.
Consequências do Controlador
Ao receber uma requisição, um módulo Controlador normalmente coordena e controla os elementos
responsáveis pela produção da resposta. Imagine uma orquestra com um maestro e vários músicos. Ela
conta com um maestro (controlador) que comanda o momento emque cada músico deve entrar em ação,
mas ele mesmo não toca nenhum instrumento.
Padrões de Projetos de Software com Java
Marcio Quirino - 110
Da mesma forma, um módulo Controlador é o grande orquestrador de um conjunto de objetos – cada
qual com sua responsabilidade específica na produção da resposta ao evento. Um problema que pode
ocorrer com esse padrão é alocar ao Controlador responsabilidades além da orquestração, como se o
maestro, além de comandar os músicos, também ficasse responsável por tocar o piano, a flauta, o violino e
outros instrumentos.
A concentração de responsabilidades reduz a coesão do controlador.
Uma importante consequência da utilização desse padrão é que os componentes de interface com o
usuário não devem assumir a responsabilidade do tratamento de eventos lógicos de sistema. Eles devem
apenas capturar as ações do usuário na interface e traduzi-las em um evento lógico de sistema a ser tratado
por algum Controlador.
O Controlador Fachada (ou Controlador por Caso de Uso) corresponde à aplicação do padrão de
projeto GoF Facade, pois essa classe de controle fornece uma interface de alto nível para a camada de
interface com o usuário, isolando-a dos componentes internos da lógica do sistema.
3. Padrões Acoplamento Baixo e Polimorfismo
O padrão Acoplamento Baixo
Um acoplamento corresponde ao grau de dependência de um módulo em relação a outros do
sistema. Um módulo com acoplamento alto depende de vários outros módulos e tipicamente apresenta
problemas, como, por exemplo:
• Propagação de mudanças pelas relações de dependência, isto é, a mudança em um módulo
causa um efeito cascata de mudanças nos módulos dependentes.
• Dificuldade para entender um módulo isoladamente.
• Dificuldade para reusar um módulo em outro contexto por exigir a presença dos diversos
módulos que formam a sua cadeia de dependências.
Portanto, quando você estiver dividindo as responsabilidades pelos módulos de um software, pense
sempre nos impactos que uma mudança pode provocar. Nesse sentido, podemos diferenciar os sistemas
conforme a seguir:
A. Acoplamento baixo
✓ As mudanças geram um impacto em poucas classes.
B. Acoplamento alto
✓ As mudanças criam um efeito dominó que impacta muitas classes.
Atenção
Outra questão importante em relação ao acoplamento é a natureza das dependências. Se uma classe A
depende de uma classe B, diz-se que A depende da implementação concreta presente em B. Por outro lado, se uma
classe A depende de uma interface I, é dito que A depende de uma abstração, uma vez que A poderia trabalhar com
diferentes implementações concretas de I sem depender diretamente de nenhuma implementação específica.
Em geral, sistemas mais flexíveis são construídos quando fazemos com que as implementações
(classes) dependam de abstrações (interfaces). Isso é uma realidade especialmente nos casos em que a
interface abstrai diferentes possibilidades de implementação, como:
A. Envolver diferentes soluções tecnológicas
✓ Como, por exemplo, as soluções de armazenamento e recuperação de dados.
B. Contar com distintas questões de negócio
✓ No caso, por exemplo, de diferentes regras de negócio e fornecedores de uma solução de
pagamento on-line.
Padrões de Projetos de Software com Java
Marcio Quirino - 111
Um sistema orientado a objetos é composto por vários objetos com responsabilidades
específicas e bem definidas.
A complexidade do sistema emerge das relações de colaboração que estabelecemos entre os
objetos. Quando projetamos um mecanismo de colaboração, naturalmente definimos relações de
dependência e, portanto, geramos acoplamento entre os elementos envolvidos. Desse modo, o objetivo não
é simplesmente minimizar dependências, e sim construir uma estrutura adequada e equilibrada delas. Cuida-
se especialmente da natureza das dependências estabelecidas, ou seja, é dada uma preferência à presença
das dependências em relação às abstrações em detrimento das dependências quanto às implementações
específicas.
A pergunta que esse padrão tenta responder, portanto, é esta: como definir relações de dependência
entre as classes de um sistema de forma a manter o acoplamento baixo, minimizar o impacto de mudanças
e facilitar o reuso?
Solução do Acoplamento Baixo
A solução proposta por esse padrão consiste em distribuir as responsabilidades a fim de gerar um
acoplamento baixo entre os módulos.
Você sabe como se mede o grau de acoplamento entre os módulos?
O grau de acoplamento está relacionado à forma com que uma relação de dependência é
estabelecida entre dois módulos. A seguir, conheceremos tais formas do nível mais alto para o mais baixo
de acoplamento.
Acoplamento de conteúdo
Ele ocorre quando um módulo utiliza aspectos de implementação de outro, ferindo o princípio a
estabelecer que um módulo deve ocultar dos demais suas decisões de implementação, de forma que seja
possível alterá-las sem se preocupar com os possíveis efeitos em outros módulos.
Veremos um exemplo de acoplamento de conteúdo. No código a seguir, a classe Data define seus
atributos “dia”, “mês” e “ano” como públicos, enquanto a classe ModuloCliente acessa diretamente o atributo
“mês” de uma instância de Data. Trata-se de um acoplamento de conteúdo, pois a forma de armazenamento
da informação de Data está exposta e é diretamente utilizada pelos demais módulos.
public class Data {
public int dia;
public int mes;
public int ano;
public int getDiaSemana() {
// implementação para retornar o dia da semana correspondente à data
}
public adicionaDias(int dias) {
// implementação para adicionar x dias à data
}
}
public class ModuloCliente {
public void metodo(Data data) {
if (data.mes == 8) {
System.out.println(“mês de agosto”);
}
}
}
Classe Data e class ModuloCliente.
Você consegue visualizar o que aconteceria com os módulos que fazem o mesmo tipo de uso da classe
Data caso resolvêssemos mudar a representação da data para texto ou para o número de dias julianos?
Padrões de Projetos de Software com Java
Marcio Quirino - 112
Acoplamento global
O acoplamento global entre dois módulos ocorre quando eles se comunicam por intermédio de
recursos, como, por exemplo, variáveis globais. Ao contrário de C++, o Java não possui uma sintaxe para a
definição de variáveis globais, mas é possível atingir um efeito similar ao se declarar em uma classe um
atributo com os modificadores public static.
O código adiante exibe um exemplo de acoplamento global. A classe Globais define um conjunto de
atributos que pode ser acessado globalmente.
public class Globais {
public static int limiteParaSaque = 500;
public static String nomeBanco;
}
public class ModuloA {
public void operacaoA() {
if (valorno qual existem os
módulos de Vendas e Estoque. Os dois módulos precisam acessar e atualizar dados
referentes aos produtos vendidos na loja e, para isso, ambos utilizam a tabela Produto em
um banco de dados relacional. Dessa forma, pode-se dizer que existe um acoplamento
externo entre ambos. Você consegue visualizar esse acoplamento?
B. Segunda situação
✓ Suponha agora que você seja o responsável pelo módulo de Vendas e que outro
desenvolvedor, responsável pelo de Estoque, resolva mudar a tabela Produto, criando alguns
campos e modificando outros existentes. Essas mudanças podem trazer impactos para o
módulo Vendas? Possivelmente sim, não é mesmo? Esses impactos são a evidência do
acoplamento resultante do uso de recursos externos compartilhados.
Acoplamento de controle
Ocorre quando um módulo controla a lógica interna de outro por meio da passagem de alguma
informação de controle. O código a seguir ilustra um exemplo de acoplamento de controle.
A operação obterProduto da classe ProdutoRepository pode recuperar dados de um produto a partir
de um arquivo local, de um arquivo via FTP ou de um banco de dados relacional. Já a classe ModuloCliente
Padrões de Projetos de Software com Java
Marcio Quirino - 113
chama a operação obterProduto, passando uma informação de controle que indica de onde deve ser feita a
leitura.
public class ProdutoRepository {
public static final int ARQUIVO_LOCAL = 1;
public static final int ARQUIVO_FTP = 2;
public static final int BANCO_RELACIONAL = 3;
public Produto obterProduto(int origem, String chave) {
switch (origem) {
case ARQUIVO_LOCAL:
return obterProdutoViaArquivoLocal (chave);
case ARQUIVO_FTP:
return obterProdutoViaArquivoFTP (chave);
case BANCO_RELACIONAL:
return obterProdutoViaBancoRelacional (chave);
}
}
private Produto obterProdutoViaArquivoLocal (String chave) {
// implementação da operação de leitura do Produto via arquivo local
}
private Produto obterProdutoViaArquivoFTP (String chave) {
// implementação da operação de leitura do Produto via arquivo FTP
}
private Produto obterProdutoViaBancoRelacional (String chave) {
// implementação da operação de leitura do Produto via banco relacional
}
}
public class ModuloCliente {
public void operacaoCliente(ProdutoRepository repositorio) {
Produto p = repositorio.obterProduto (ProdutoRepository.BANCO_RELACIONAL,
“1234”);
// restante da implementação da operação viria aqui
}
}
Classe ProdutoRepository e class ModuloCliente.
Observe que o módulo cliente não apenas está exposto a todas as implementações do módulo de
leitura de dados do produto, como também controla esse módulo, indicando a implementação desejada.
Uma solução menos acoplada teria o módulo cliente dependendo de uma abstração, ou seja, de um serviço
capaz de obter um produto, sem que ele precise saber que existem diferentes implementações para esse
serviço.
Acoplamento de estrutura
Ele acontece quando um módulo chamador passa uma estrutura de dados para um módulo chamado,
o qual, por sua vez, utiliza apenas um pequeno subconjunto de dados dessa estrutura. O código à frente
ilustra um exemplo de acoplamento de estrutura.
A classe Pedido contém todas as informações de um pedido. Já a classe CalculadoraFrete possui
uma operação que calcula o frete de um pedido recebido como parâmetro. Entretanto, o algoritmo de cálculo
do frete utiliza apenas o endereço de destino, o que configura um exemplo de utilização de uma pequena
parcela (endereço de destino) de uma estrutura de dados (Pedido).
public class Pedido {
private Date data;
private int local;
private List itens;
private Cliente cliente;
private Endereco enderecoEntrega;
private Pagamento;
// restante da classe Pedido
}
Padrões de Projetos de Software com Java
Marcio Quirino - 114
public class CalculadoraFrete {
public BigDecimal obterValorFrete(Pedido pedido) {
// algoritmo de cálculo do valor de frete é feito utilizando apenas
// o endereço de destino do pedido
}
}
public class Exemplo {
CalculadoraFrete calculadoraFrete;
public void fecharPedido(Pedido pedido) {
calculadoraFrete.obterValorFrete(pedido);
}
}
Classe Pedido, classe CalculadoraFrete e classe Exemplo.
Acoplamento de dados
O acoplamento de dados ocorre quando um módulo se comunica com outro, passando apenas os
dados dos quais o módulo chamado precisa para cumprir a sua responsabilidade. Nesse tipo de
acoplamento, os módulos são independentes e se comunicam por meio de dados.
O código adiante mostra um exemplo de acoplamento de dados no qual o módulo CalculadoraFrete
recebe o endereço de destino, que é a informação necessária para calcular o frete.
public class Pedido {
private Date data;
private int local;
private List itens;
private Cliente cliente;
private Endereco enderecoEntrega;
private Pagamento pagamento;
// restante da classe Pedido
}
public class CalculadoraFrete {
public BigDecimal obterValorFrete(Endereco enderecoDestino) {
// algoritmo de cálculo do valor de frete é feito utilizando apenas
// o endereço de destino do pedido
}
}
public class Exemplo {
CalculadoraFrete calculadoraFrete;
public void fecharPedido(Pedido pedido) {
calculadoraFrete.obterValorFrete(pedido.getEnderecoDestino());
}
}
Classe Pedido, classe CalculadoraFrete e classe Exemplo.
Portanto, os níveis de acoplamento, do mais alto para o mais baixo, são:
• Conteúdo;
• Global;
• Externo;
• Controle;
• Estrutura;
• Dados.
Padrões de Projetos de Software com Java
Marcio Quirino - 115
Ao dividir as responsabilidades pelos módulos, devem ser balanceadas três heurísticas ligadas às
relações de dependência entre eles:
A. Heurística 1
✓ Minimizar o número de dependências entre os módulos sem comprometer a coesão, o que
significa dizer que dependências desnecessárias não devem ser criadas.
B. Heurística 2
✓ Utilizar, sempre que possível, os níveis mais baixos de acoplamento, preferencialmente o
acoplamento de dados.
C. Heurística 3
✓ Fazer com que as implementações dependam das abstrações, especialmente nas relações
de dependência entre os módulos que implementam a lógica de negócio e aqueles
relacionados à tecnologia, como, por exemplo, interface gráfica com o usuário,
armazenamento de dados e integração com componentes externos.
Consequências do Acoplamento Baixo
O acoplamento é um princípio fundamental da estruturação de software. Ele, por isso, precisa ser
considerado em qualquer decisão de projeto de software.
Você sabe como os acoplamentos são criados concretamente em um programa?
Em linguagens como Java, por exemplo, um acoplamento direto entre o módulo X e o módulo Y é
criado quando:
1. Y é utilizado como o tipo de um atributo definido em X.
2. Um método definido em X invoca operações definidas em Y (operações com escopo de
classe).
3. Um método definido em X utiliza uma instância de Y, seja ela criada localmente no método,
recebida como parâmetro do método ou obtida com o retorno de uma operação invocada
dentro desse método.
4. Uma instância de Y é criada dentro de X via inicialização ou dentro de algum método
específico de X.
5. X é descendente(herda direta ou indiretamente) de Y.
6. Y é uma interface, e X a implementa.
Além disso, os acoplamentos indiretos podem ser definidos pelo compartilhamento de recursos,
como variáveis globais, arquivos e bancos de dados, por exemplo. Sempre que você utilizar uma dessas
construções, um acoplamento será definido.
Sendo assim, avalie com cuidado se esse acoplamento é realmente necessário ou se há alternativas
que levariam a um menor acoplamento. Observe ainda se você pode criar alguma abstração para não gerar
uma dependência de uma implementação específica.
Atenção
Em geral, manter as classes de domínio isoladas e não dependentes de tecnologia – de armazenamento, de
interface com usuário ou de integração entre sistemas, entre outras opções – constitui uma política geral de
acoplamento que deve ser seguida e que está presente em proposições, como, por exemplo, a arquitetura hexagonal
ou a limpa.
O padrão Polimorfismo
Suponha que você esteja implementando os requisitos de pagamento em cartão de uma loja on-line.
Para implementar um pagamento em cartão que interaja diretamente com uma administradora de cartão, é
preciso passar por um longo e complexo processo de homologação perante a administradora. Imagine então
realizar esse processo com cada uma!
Padrões de Projetos de Software com Java
Marcio Quirino - 116
Comentário
Atualmente existem diferentes brokers de pagamento homologados perante as diversas administradoras que
fornecem uma API para a integração com sistemas de terceiros. No entanto, cada broker tem uma política de preços;
eventualmente, podem surgir novos brokers com políticas mais atrativas no mercado.
Agora imagine que você seja o responsável por fornecer uma solução de sistema para diferentes
lojistas que podem escolher os brokers de pagamento em função das suas exigências de segurança, preço
e volume de transações, entre outros fatores de influência. Isso significa que o nosso software tem de ser
capaz de funcionar com diferentes brokers, cada um com a sua API proprietária.
Quem não conhece polimorfismo resolveria esse problema com uma solução baseada em if-then-
else ou switch-case, sendo cada alternativa de broker mapeada em um case no switch ou em um else no if-
then-else, como ilustra o trecho de código a seguir. Pense como ficaria tal código se houvesse vinte brokers
diferentes!
public class FechamentoPedido {
private ConfiguracaoSistema config;
public FechamentoPedido(ConfiguracaoSistema config) {
this.config = config;
}
public BigDecimal fecharPedido(Pedido pedido) {
if (config.brokerPagamento == ConfiguracaoSistema.BROKER_1) {
config.broker1.efetuarPagamento_Broker1();
else if (config.brokerPagamento == ConfiguracaoSistema.BROKER_2) {
config.broker2.efetuarPagamento_Broker2();
else if (config.brokerPagamento == ConfiguracaoSistema.BROKER_3) {
config.broker3.efetuarPagamento_Broker3();
}
}
}
Classe FechamentoPedido.
Esse problema possui uma complicação adicional, já que cada broker fornece uma API proprietária
com interfaces diferentes. Os sufixos Broker1, Broker2 e Broker3 nos nomes das operações representam o
fato de que as operações de cada API não possuem o mesmo nome e que também podem diferir nos
argumentos passados na chamada de cada operação. Em resumo, o padrão Polimorfismo procura resolver
os seguintes problemas:
1. Como produzir uma solução genérica para alternativas baseadas no tipo de um elemento
criando módulos plugáveis?
2. Como evitar construções condicionais complexas contendo um código condicional baseado
no tipo do objeto ou em alguma propriedade específica?
Comentário
No nosso exemplo, as alternativas baseadas no tipo de um elemento correspondem aos diferentes tipos de
brokers com as suas APIs proprietárias.
Solução do Polimorfismo
Você percebeu como a solução baseada em estruturas condicionais do tipo if-then-else ou switch-
case, além de ser mais complexa, cria um acoplamento do módulo chamador com cada implementação
específica? Note como a classe FechamentoPedido depende diretamente de todas as implementações de
broker de pagamento. Você se lembra de que um princípio geral de bons projetos orientados a objetos é
haver implementações dependentes de abstrações, especialmente em casos nos quais essas abstrações
possam ter várias implementações?
Padrões de Projetos de Software com Java
Marcio Quirino - 117
A solução via polimorfismo consiste em criar uma interface genérica para a qual podem existir
diversas implementações específicas.
A estrutura condicional é substituída por uma única chamada, a qual, aliás, é feita utilizando essa
interface genérica. O chamador, portanto, não precisa conhecer a classe que está do outro lado da interface
provendo a implementação. A capacidade de um elemento (a interface genérica) poder assumir diferentes
formas concretas (broker1, broker2 ou broker3, no exemplo anterior) é conhecida como polimorfismo.
O código adiante ilustra a estrutura de solução para o exemplo do fechamento do pedido com a
utilização do padrão Polimorfismo.
public interface BrokerPagamento {
void efetuarPagamento(Pedido pedido);
}
public class Broker_1_Adapter implements BrokerPagamento {
private Broker1 broker1 = new Broker1();
public void efetuarPagamento(Pedido pedido) {
// código específico para utilização do broker 1
broker1.efetuarPagamento_Broker1();
}
}
public class Broker_2_Adapter implements BrokerPagamento {
private Broker1 broker2 = new Broker1();
public void efetuarPagamento(Pedido pedido) {
// código específico para utilização do broker 2
broker2.efetuarPagamento_Broker2();
}
}
public class FechamentoPedido {
private ConfiguracaoSistema config;
public class FechamentoPedido (ConfiguracaoSistema config) {
this.config = config;
}
public BigDecimal fecharPedido(Pedido pedido) {
config.broker.efetuarPagamento(); // chamada polimórfica
}
}
Public class Exemplo {
public void executar(Pedido pedido) {
ConfiguracaoSistema config = new ConfiguracaoSistema();
config.setBroker(new Broker_1_Adapter());
FechamentoPedido fechamento = new FechamentoPedido(config);
Fechamento.fecharPedido(pedido);
}
}
Fechamento do pedido com a utilização do padrão Polimorfismo.
Vamos analisar o código:
1. Criação da interface BrokerPagamento
✓ Em vez de a classe FechamentoPedido utilizar diretamente todos os brokers, é criada uma
interface BrokerPagamento para separar o módulo cliente dos módulos que implementam
essa interface.
2. Criação de adaptadores
✓ Como cada broker possui uma interface específica, é criado um conjunto de adaptadores que
implementa a interface genérica BrokerPagamento e faz a conversão entre a interface
genérica e a específica de cada broker. Desse modo, um adaptador de interface é criado para
cada broker de pagamento.
Padrões de Projetos de Software com Java
Marcio Quirino - 118
3. Implementação da operação efetuarPagamento
✓ Verifique como a classe Broker_1_Adapter implementa a operação efetuarPagamento
chamando uma operação específica da API do broker 1. De forma análoga, a classe
Broker_2_Adapter opera com a API do broker 2. Observe ainda como o código condicional
anteriormente presente na implementação da classe FecharPedido foi substituído por uma
simples chamada à operação efetuarPagamento de um objeto que implementa a interface
BrokerPagamento.
Dica
Como esse objeto pode assumir múltiplas formas112
Acoplamento externo .......................................................................................................... 112
Acoplamento de controle..................................................................................................... 112
Acoplamento de estrutura ................................................................................................... 113
Acoplamento de dados........................................................................................................ 114
Consequências do Acoplamento Baixo .............................................................................................. 115
O padrão Polimorfismo ........................................................................................................................... 115
Solução do Polimorfismo .................................................................................................................... 116
Consequências do Polimorfismo ........................................................................................................ 118
4. Padrões Invenção Pura, Indireção e Variações Protegidas................................................ 118
O padrão Invenção Pura ........................................................................................................................ 118
Solução da Invenção .......................................................................................................................... 119
Consequências da Invenção Pura ...................................................................................................... 120
O padrão Indireção ................................................................................................................................. 120
Solução da Indireção .......................................................................................................................... 121
Consequências da Indireção .............................................................................................................. 122
O padrão Variações Protegidas ............................................................................................................. 122
Solução das Variações Protegidas ..................................................................................................... 123
Consequências das Variações Protegidas ......................................................................................... 125
Considerações finais .............................................................................................................................. 125
Explore + ................................................................................................................................................ 125
Referências ............................................................................................................................................ 126
Tecnologias JPA e JEE ..................................................................................................................... 127
Introdução .............................................................................................................................................. 127
1. JPA no desenvolvimento web .............................................................................................. 127
Mapeamento objeto-relacional ............................................................................................................... 127
ORM (mapeamento objeto-relacional)................................................................................................ 127
Entity Beans ....................................................................................................................................... 128
Hibernate ............................................................................................................................................ 128
Java Persistence API ............................................................................................................................. 129
Definindo uma entidade JPA .............................................................................................................. 129
Consulta e manipulação de dados ......................................................................................................... 131
Padrões de Projetos de Software com Java
Marcio Quirino - 6
Entity Manager ................................................................................................................................... 131
Inclusão de dados .............................................................................................................................. 132
Exclusão de dados ............................................................................................................................. 132
Execução do aplicativo ........................................................................................................................... 133
Adicionando a biblioteca JDBC e o framework EclipseLink................................................................ 133
Criando o banco de dados Derby ....................................................................................................... 135
Resultado da execução do aplicativo ................................................................................................. 136
Manipulando dados com NamedQueries ............................................................................................... 136
Roteiro de prática ............................................................................................................................... 137
2. Implementação de regras de negócio com EJBs ................................................................ 137
Enterprise Java Beans (EJB) ................................................................................................................. 137
Session beans ........................................................................................................................................ 139
Stateless e stateful ............................................................................................................................. 139
Interface de acesso ............................................................................................................................ 140
Singleton ............................................................................................................................................ 140
Session bean com Servlet .................................................................................................................. 140
Message-driven beans (MDB) ................................................................................................................ 141
Mensagerias ....................................................................................................................................... 141
Modelo publish/subscribe .................................................................................................... 141
Modelo point to point ........................................................................................................... 141
MDB (message-driven bean) .............................................................................................................. 142
Aplicativo corporativo ............................................................................................................................. 144
EJBs ................................................................................................................................................... 144
MDBs..................................................................................................................................................em tempo de execução, pois ele pode ser a instância de
qualquer um dos adaptadores para os brokers específicos, dizemos que essa chamada é polimórfica.
Você percebeu que o polimorfismo permite adicionar novas soluções de broker sem que haja a
necessidade de alterar o módulo chamador (FechamentoPedido)? Basta, para tal, implementar um novo
adaptador e adicioná-lo à configuração do sistema, pois o restante do sistema já está preparado para
trabalhar com esse novo broker.
Consequências do Polimorfismo
O Polimorfismo é um princípio fundamental em projetos de software orientados a objetos que nos
ajuda a resolver, de forma sintética, elegante e flexível, o problema de se lidar com variantes de
implementação de uma mesma operação conceitual. O conceito dele está presente na definição de diversos
padrões GoF. Listaremos alguns deles a seguir:
• Adapter;
• Command;
• Composite;
• Proxy;
• State;
• Strategy;
Entretanto, é preciso ter cuidado para não construir estruturas genéricas para situações nas quais
não haja uma possibilidade de variação. Uma solução genérica se mostra mais flexível, mas é preciso estar
atento a fim de não investir um esforço na produção de soluções genéricas para os problemas que sejam
específicos por natureza, ou seja, que não apresentem variantes de implementação.
4. Padrões Invenção Pura, Indireção e Variações Protegidas
O padrão Invenção Pura
Você está desenvolvendo um sistema de vendas para uma loja on-line e identificou algumas classes
que correspondem aos conceitos do negócio, tais como Produto, Cliente e Pedido. O padrão Especialista
recomenda que uma responsabilidade seja atribuída ao módulo que possua o conhecimento necessário
para realizá-la. Vimos que o cálculo do valor total do Pedido envolve o conhecimento de todos os itens e os
seus respectivos valores. Ele, assim, deveria ser responsabilidade da classe Pedido.
Qual classe deveria ser a responsável por salvar os dados do Pedido em um banco de dados
relacional?
Seguindo o padrão Especialista, deveríamos alocar essa responsabilidade na classe detentora das
informações, que é a própria classe Pedido. Contudo, essa solução comprometeria o padrão Coesão Alta,
já que alocaríamos em um mesmo módulo responsabilidades de diferentes naturezas, ou seja, a lógica de
negócio e de armazenamento em banco de dados relacional.
Padrões de Projetos de Software com Java
Marcio Quirino - 119
Solução da Invenção
A solução proposta pelo padrão Invenção Pura diz respeito à criação de classes artificiais, isto é,
classes que não representam um conceito do domínio do problema, para gerar soluções com coesão alta e
acoplamento baixo. O problema do armazenamento do Pedido em um banco de dados pode ser
implementado em uma classe PedidoRepository, a qual, por sua vez, fica responsável pelas operações
básicas com pedidos em um meio de armazenamento como, por exemplo, armazenar um novo pedido,
atualizar os dados de um já existente, remover um pedido e recuperar um ou mais pedidos.
O código a seguir ilustra a implementação da classe Pedido com os atributos e as responsabilidades
ligados ao domínio do negócio e à classe PedidoRepository, que é uma “Invenção Pura”, definida com a
responsabilidade de armazenar e recuperar pedidos de um banco de dados relacional. Com isso, é gerada
uma solução com módulos de coesão alta, já que as funcionalidades de diferentes propósitos estão
segregadas em classes distintas.
public class Pedido {
private int numero;
private java.util.Date data;
private List itens;
public BigDecimal obterValorTotal() {
BigDecimal resultado = new BigDecimal(0);
itens.forEach(item -> {
resultado.add(item.obterValor());
});
return resultado;
}
public int getNumero() {
return numero;
}
public void setNumero(int numero) {
this.numero = numero;
}
public Date getData() {
return data;
}
public void setData(Date data) {
this.data = data;
}
public Iterator getItens() {
return itens.iterator();
}
public void adicionarItem(int quantidade, Produto produto) {
itens.add(new ItemPedido(itens.size() + 1, quantidade, produto));
}
}
public class PedidoRepository {
public void inserirPedido(Pedido pedido) {
// monta e executa SQL INSERT com os dados do pedido
}
public void atualizarPedido(Pedido pedido) {
// monta e executa SQL UPDATE dos dados do pedido
}
public void excluirPedido(Pedido pedido) {
// monta e executa SQL DELETE do registro do pedido
}
public Pedido recuperarPedidoPorNumero(int numero) {
// monta e executa SQL SELECT dos dados do pedido
// monta objeto Pedido a partir dos dados recuperados do banco de dados
// retorna objeto Pedido
}
}
Classe Pedido.
Padrões de Projetos de Software com Java
Marcio Quirino - 120
Consequências da Invenção Pura
Esse padrão permite a criação de soluções com módulos de coesão alta e acoplamento baixo, sendo
particularmente útil nas situações em que a heurística padrão para a aplicação do padrão Especialista na
Informação gera módulos de coesão baixa.
A criação de classes correspondentes a conceitos e elementos concretos encontrados no domínio
do negócio é um princípio natural de projetos orientados a objetos. Essa estratégia é chamada de
decomposição por representação. Já a criação de classes por Invenção Pura resulta da estratégia chamada
decomposição por comportamento. Vamos entender a diferença:
1. Decomposição por representação
✓ Essa estratégia visa produzir módulos que representem os elementos do domínio sem uma
transformação abruta do universo real para a sua representação em software.
2. Decomposição por comportamento
✓ Essa estratégia é aplicada nas situações em que se desejar agrupar comportamentos ou
algoritmos sem necessariamente vinculá-los diretamente a classes que representam os
conceitos do domínio do negócio.
Exemplo
A loja on-line pode ter diferentes políticas de desconto para pedidos dependendo da época do ano, do total do
pedido, do endereço de entrega ou da frequência de compras do cliente. Em vez de colocar todos esses algoritmos na
classe Pedido, que é a detentora primária das informações do pedido, separam-se essas políticas em uma família de
classes correspondente aos diversos algoritmos de cálculo de desconto aplicando o padrão Invenção Pura. Como
consequência, pode-se evoluir os algoritmos de desconto de forma independente da classe Pedido, gerando módulos
com coesão mais alta e uma solução mais flexível.
É importante equilibrar as duas estratégias (decomposição por representação e por comportamento).
Quando a estratégia de decomposição por representação está sendo subutilizada, surgem sinais, como, por
exemplo, classes do domínio do problema contendo apenas atributos e operações de acesso (get/set) ou
algumas classes concentrando a lógica de negócio em muitos métodos com dezenas ou centenas de linhas
de código.
Padrões GoF, como o Adapter, o Strategy e o Command, são alguns exemplos de aplicação do
padrão Invenção Pura, pois eles são baseados na decomposição de responsabilidades por comportamento.
O padrão Indireção
Indireção é uma das técnicas mais simples e mais utilizadas em projetos de software. Ela pode ser
utilizada em diversas situações.
Veja a seguir alguns problemas que podem ser solucionados pelo padrão Indireção:
1. Comunicação entre objetos remotos
✓ Um objeto A precisa chamar uma operação de um objeto remoto B. Como fazercom que A
não precise lidar com questões, como sockets, serialização da mensagem e interpretação
dos dados de retorno, entre outros aspectos inerentes à comunicação em rede?
2. Utilização de API de terceiros
✓ Um objeto A deve chamar uma operação de um serviço B cujo acesso é fornecido por um
fornecedor terceiro. Por isso, não é possível modificar o código da API de acesso ao serviço
B. Imagine que eventualmente você tenha de trabalhar com outros fornecedores, cada qual
com a sua API proprietária. Como isolar o objeto A das diferenças entre as APIs dos diversos
fornecedores?
3. Abstração de tecnologia
✓ Você precisa conectar diversos componentes clientes a uma camada de serviços que pode
ser implementada em diferentes tecnologias, como, por exemplo, EJB, Web Services e REST
Padrões de Projetos de Software com Java
Marcio Quirino - 121
API. Como fazer para que os clientes não precisem ser alterados em função de uma mudança
na tecnologia de implementação da camada de serviços?
4. Redução de acoplamento
✓ Um objeto A que captura uma interação do usuário com o sistema tem de interagir com
diversos objetos de negócio para cumprir sua função. Como reduzir o acoplamento de A com
os diversos objetos da camada de negócio, tornando a implementação desse objeto menos
complexa?
Solução da Indireção
A solução proposta pelo padrão Indireção consiste em substituir a conexão direta entre dois ou mais
objetos por uma estrutura de comunicação mediada por um objeto intermediário. Assim, se um objeto A
enviar uma mensagem diretamente para B, ele passará a mandá-la para um objeto intermediário X, o qual,
por sua vez, fica responsável pela comunicação com B.
Observemos como o padrão Indireção pode ser utilizado nas situações descritas anteriormente:
1. Comunicação entre objetos remotos
✓ Para isolar um objeto A cliente das complexidades de interação com um objeto remoto B,
pode-se utilizar o padrão Gof Proxy, que consiste em definir um par de objetos (proxy e stub)
entre os objetos A e B. O objeto proxy roda no mesmo processo de A oferecendo as mesmas
operações de B, permitindo que A possa chamar operações de B como se eles estivessem
rodando no mesmo processo. Já o objeto stub roda no mesmo processo de B, recebendo as
requisições do proxy e repassando-as para o objeto B. Dessa forma, os objetos proxy e stub
cuidam dos aspectos da comunicação remota, reduzindo a complexidade da implementação
dos módulos A e B.
2. Utilização de API de terceiros
✓ Quando um objeto A depende de um serviço que pode ter diversas implementações
fornecidas por diferentes terceiros, deve-se segregar A desses fornecedores por intermédio
da inserção de objetos adaptadores. Esses objetos traduzem uma interface comum
conhecida por A nas chamadas proprietárias fornecidas por cada API. Essa é a solução
proposta pelo padrão GoF Adapter.
3. Abstração de tecnologia
✓ Padrões J2EE, como Service Locator e Business Delegate, são exemplos de solução para o
problema da abstração de tecnologia. Eles encapsulam a forma com que um serviço é
localizado e a tecnologia utilizada na sua implementação (exemplos: EJB, web service e
REST API), fazendo com que todos os objetos clientes desse serviço se comuniquem com
um objeto intermediário (Business Delegate) de forma independente de tecnologia. Esse
objeto delega a chamada para o objeto real utilizando a tecnologia específica de
comunicação. Assim, quando essa tecnologia mudar, basta alterar o Business Delegate, pois
os clientes estão isolados do impacto dessa mudança.
4. Redução de acoplamento
✓ Dois padrões GoF (Facade e Mediator) têm o propósito de reduzir o acoplamento entre
objetos. O padrão Facade oferece uma interface de alto nível para clientes de um subsistema,
reduzindo o número de objetos com os quais eles precisam interagir. Já o padrão Mediator
reduz uma rede de dependências entre objetos do tipo N x N para uma topologia 1 x N,
substituindo as comunicações diretas entre os objetos por aquelas mediadas por um objeto
intermediário responsável por receber as notificações dos objetos e encaminhar o
processamento para os objetos correspondentes.
Você conseguiu perceber como diversos padrões de projeto GoF são aplicações específicas
do padrão Indireção?
Padrões de Projetos de Software com Java
Marcio Quirino - 122
Consequências da Indireção
Indireção é um princípio fundamental muito utilizado em padrões de projeto.
Trata-se de introduzir uma camada entre o cliente e o fornecedor a fim de desacoplá-los,
promovendo soluções mais flexíveis, reusáveis e mais fáceis de estender e evoluir.
O padrão Indireção permite:
1. Isolar o cliente das tecnologias específicas utilizadas pelo fornecedor e das variações nos
fornecedores de serviços consumidos pelo cliente.
2. Reduzir o acoplamento entre os módulos.
Além de ser amplamente utilizado em padrões de projeto, esse padrão também é utilizado em áreas,
como, por exemplo, balanceamento de carga e PAAS (Platform as a Service).
Atenção
A adição de um ou mais níveis de indireção pode causar algum impacto na performance do sistema. Embora,
para a maioria dos sistemas de informação, o impacto não seja relevante, nos sistemas cujo tempo de resposta seja
muito crítico, como jogos e sistemas de controle em tempo real, por exemplo, o uso excessivo de indireções poderá
penalizar a performance.
O padrão Variações Protegidas
Você sabe qual é um dos maiores pesadelos de quem precisa evoluir um sistema tendo de atender
a novas demandas em um prazo cada vez mais reduzido? É quando uma alteração aparentemente simples
para o cliente demanda um enorme esforço de implementação, pois diversos módulos precisarão ser
modificados para atender a esse novo requisito. Essa é uma situação claramente indesejável, ocorrendo em
sistemas que possuem uma estrutura frágil. Mas o que causa tal fragilidade?
Resposta
Essa fragilidade é resultante das relações de dependência e da forma como organizamos as decisões nos
módulos que criamos. Sempre que se projeta um sistema, é importante avaliar os aspectos que podem variar e as
consequências resultantes dessas variações.
Pense em um sistema da operação de bancos. Trata-se daquele que permite alguém ter uma conta
corrente e fazer depósitos, saques, transferências, por exemplo. Essas operações existem há décadas.
Agora reflita: nos últimos 20 anos, o que mudou nesses sistemas?
Um aspecto que claramente vem mudando com frequência nesse período está ligado às tecnologias
utilizadas para o cliente interagir com o sistema: caixa eletrônico, internet banking e aplicativo no celular são
só alguns exemplos.
Outro aspecto de mudança diz respeito às regras de execução dessas operações, como ocorre nos
limites para saques ou nas retiradas limitadas pelo local onde você faça tal operação. Existem, enfim, muitas
regras relacionadas à segurança das operações.
Nesse pequeno exemplo, você já percebe que há pelo menos dois aspectos sujeitos a variações:
A. Aspectos de interação com o usuário
✓ A forma com que ele solicita uma transferência de valores, por exemplo, em cada interface
onde ela esteja disponível.
B. Regras de negócio
✓ As políticas e regras que precisam ser observadas em cada operação desempenhada.
Sabendo que esses elementos estão sujeitos a variações, nossa missão, como projetistas, é
estruturar os módulos a fim de que essas variações possam ser acomodadas com um impacto localizado
Padrões de Projetos de Software com Java
Marcio Quirino - 123
em um pequeno número de módulos, gerando, com isso, uma estrutura flexível e adaptável às mudanças
cuja ocorrência julgamos possível.
Vamos ilustrar essa questão com um exemplo. Você está implementando um módulo do sistema de
vendas on-line responsável por coordenar o fechamento do pedido realizado pelo cliente. Uma das tarefas
desse módulo é solicitar o armazenamento do pedido àquele responsávelpela persistência dos pedidos.
Uma possível implementação para essa tarefa será ilustrada no código a seguir.
public class ServicoFechamentoPedido {
private PedidoRepository;
public ServicoFechamento(PedidoRepository repository) {
this.pedidoRepository = repositor;
}
public fecharPedido(Pedido pedido) {
...
pedidoRepository.inserirPedido(pedido);
...
}
}
public class PedidoRepository {
public void inserirPedido(Pedido pedido) {
// monta e executa SQL INSERT com os dados do pedido
}
public void atualizarPedido(Pedido pedido) {
// monta e executa SQL UPDATE dos dados do pedido
}
public void excluirPedido(Pedido pedido) {
// monta e executa SQL DELETE do registro do pedido
}
public Pedido recuperarPedidoPorNumero(int numero) {
// monta e executa SQL SELECT dos dados do pedido
// monta objeto Pedido a partir dos dados recuperados do banco de dados
// retorna objeto Pedido
}
}
Classe ServicoFechamentoPedido e classe PedidoRepository.
Vimos no código que a classe ServicoFechamentoPedido recebe um repositório (PedidoRepository)
para o qual ele solicita a execução da operação inserirPedido. A classe PedidoRepository é uma
implementação que faz o armazenamento e a recuperação de pedidos de um banco de dados relacional.
Nessa solução, uma implementação (módulo cliente ServicoFechamentoPedido) está acoplada a
outra (módulo fornecedor PedidoRepository), o que compromete a flexibilidade da solução.
Solução das Variações Protegidas
A solução proposta pelo padrão Variações Protegidas é identificar pontos sujeitos à variação e isolar
essas variações com a criação de interfaces estáveis no seu entorno.
Suponha que a nossa loja on-line tenha de funcionar com pedidos armazenados em diferentes
servidores de bancos de dados, ou seja, nosso sistema tem de se adaptar ao servidor de banco de dados
do lojista, que pode ser:
A. Relacional
✓ Como Oracle, Microsoft SQL Server e MySQL.
B. NoSQL
✓ Como MongoDB, Redis e Cassandra.
Na solução original, a classe ServicoFechamentoPedido depende diretamente de uma
implementação concreta que acessa bancos de dados relacionais.
Padrões de Projetos de Software com Java
Marcio Quirino - 124
Mas como poderemos projetar a solução a fim de que seja possível variar a implementação
de armazenamento de pedidos sem que essa classe precise ser modificada?
Quando estabelecemos uma relação de dependência, temos de avaliar se o módulo fornecedor pode
possuir implementações alternativas. Há, por exemplo, outras formas de armazenar e recuperar pedidos
que não seja via banco de dados relacional?
Caso a resposta seja afirmativa, será conveniente isolar o cliente do fornecedor introduzindo uma
abstração. O código adiante ilustra uma solução:
public class ServicoFechamentoPedido {
private PedidoRepository pedidoRepository;
public ServicoFechamento(PedidoRepository repository) {
this.pedidoRepository = repositor;
}
public fecharPedido(Pedido pedido) {
...
pedidoRepository.inserirPedido(pedido);
...
}
}
public interface PedidoRepository {
void inserirPedido(Pedido pedido);
void atualizarPedido(Pedido pedido);
void excluirPedido(Pedido pedido);
Pedido recuperarPedidoPorNumero(int numero);
}
public class PedidoRDBMSRepository implements PedidoRepository {
public void inserirPedido(Pedido pedido) {
// monta e executa SQL INSERT com os dados do pedido
}
public void atualizarPedido(Pedido pedido) {
// monta e executa SQL UPDATE dos dados do pedido
}
public void excluirPedido(Pedido pedido) {
// monta e executa SQL DELETE do registro do pedido
}
public Pedido recuperarPedidoPorNumero(int numero) {
// monta e executa SQL SELECT dos dados do pedido
// monta objeto Pedido a partir dos dados recuperados do banco de dados
// retorna objeto Pedido
}
}
public class PedidoMongoRepository implements PedidoRepository {
public void inserirPedido(Pedido pedido) {
// monta e executa comando MongoDB para inserir os dados do pedido
}
public void atualizarPedido(Pedido pedido) {
// monta e executa comando MongoDB para alterar os dados do pedido
}
public void excluirPedido(Pedido pedido) {
// monta e executa comando MongoDB para remover os dados do pedido
}
public Pedido recuperarPedidoPorNumero(int numero) {
// monta e executa comando MongoDB para recuperar os dados do pedido
// monta objeto Pedido a partir dos dados recuperados
// retorna objeto Pedido
}
}
Padrões de Projetos de Software com Java
Marcio Quirino - 125
Na solução apresentada, nós protegemos o módulo cliente de variações na implementação do
módulo fornecedor com a criação de uma interface estável, a qual, por sua vez, deve ser implementada em
cada possível variante.
A classe ServicoFechamentoPedido passa a utilizar a interface PedidoRepository, que é
implementada em cada variante específica.
Atenção
PedidoRDBMSRepository é uma implementação das operações apresentadas em banco de dados relacional,
enquanto PedidoMongoRepository corresponde à implementação utilizando o MongoDB.
Consequências das Variações Protegidas
Um dos grandes desafios do trabalho de um arquiteto ou desenvolvedor de software é identificar e
proteger os pontos de variação de um software. O padrão Variações Protegidas constitui um dos conceitos
mais presentes em mecanismos e padrões de desenvolvimento pela flexibilidade e proteção que ele
proporciona em relação a variações em dados, comportamento, componentes e sistemas externos.
Pode-se utilizar esse padrão aplicando conceitos básicos de orientação a objetos, como
encapsulamento, interfaces e polimorfismo. Soluções mais avançadas, porém, também podem ser
combinadas, como linguagens baseadas em regras, interpretadores de regras e metaprogramação com as
APIs de Reflection e Annotation do Java. Vários padrões GoF, como o Adapter, Strategy, Abstract Factory
e o Bridge, por exemplo, fazem uso do padrão Variações Protegidas.
Atenção
É importante verificar se o esforço envolvido para projetar e implementar uma solução que proteja partes do
sistema de variações terá um retorno adequado. Dessa maneira, deve-se avaliar a probabilidade de as variações em
determinados pontos do sistema acontecerem e o impacto dessas mudanças, priorizando as variações mais prováveis
e de maior impacto.
Considerações finais
Neste conteúdo, verificamos como os padrões GRASP podem ser utilizados para distribuir as
responsabilidades do sistema entre as classes com o propósito de gerar uma solução com módulos mais
coesos e menos acoplados. Vimos ainda que o padrão Especialista na Informação estabelece uma
heurística simples de atribuição de responsabilidade baseada no conhecimento que cada classe possui.
Em seguida, pontuamos que o padrão Criador recomenda que os agregados sejam responsáveis por
criar as suas partes. Observamos também que Coesão Alta e Acoplamento Baixo são padrões
fundamentais, pois, enquanto o conceito de coesão está relacionado ao propósito específico de cada
módulo, o padrão Acoplamento visa controlar as relações de dependência entre os módulos para que os
impactos de futuras modificações sejam minimizados.
Ainda apontamos que o padrão Controladoraborda a atribuição da responsabilidade de coordenar a
produção de respostas aos eventos de sistema e que o padrão Polimorfismo é um conceito da orientação a
objetos a permitir que as implementações dependam de abstrações, e não de outras implementações
concretas. Por fim, destacamos que os padrões Indireção e Variações Protegidas sugerem mecanismos
para reduzir o acoplamento entre os objetos, permitindo que os módulos fornecedores de serviços possam
variar sem impactar os módulos clientes.
Explore +
Para saber mais sobre a programação orientada a objetos, acesse o site da DevMedia e leia este
artigo: Utilização dos princípios Solid na aplicação de padrões de projeto. Consultado na internet em: 18 out.
2021.
Padrões de Projetos de Software com Java
Marcio Quirino - 126
O site Patterns in Practice: Cohesion And Coupling discute e apresenta exemplos dos conceitos de
coesão e acoplamento.
A página Refactoring guru apresenta um conteúdo interativo e bastante completo de todos os
padrões de projeto GoF com exemplos de código em diversas linguagens de programação. Consultado na
internet em: 18 out. 2021.
Referências
GAMMA, E. et al. Design patterns: elements of reusable object-oriented software. 1. ed. Boston:
Addison-Wesley, 1994.
LARMAN, C. Applying UML and patterns: an introduction to object-oriented analysis and design and
iterative development. 3. ed. Upper Saddle River: Prentice Hall, 2004.
MARTIN, R. C. Clean architecture: a craftsman´s guide to software structure and design. 1. ed. Upper
Saddle River: Prentice Hall, 2017.
Padrões de Projetos de Software com Java
Marcio Quirino - 127
Tecnologias JPA e JEE
Introdução
Neste conteúdo, abordaremos a tecnologia JPA (Java Persistence API) para o mapeamento objeto-
relacional e o uso de componentes do tipo EJB (Enterprise Java Bean), elemento central do JEE (Java
Enterprise Edition), para a implementação de regras de negócio.
Após compreender ambas as tecnologias, analisaremos os elementos estruturais da arquitetura MVC
(Model, View e Controller) e desenvolveremos um sistema cadastral com base nessa arquitetura, utilizando
JPA, EJB e componentes web. Além disso, adotaremos o padrão Front Controller na camada de visualização
para web.
1. JPA no desenvolvimento web
Mapeamento objeto-relacional
Nos bancos de dados relacionais, a estrutura é baseada em tabelas que armazenam valores em
registros, que se relacionam a partir de campos identificadores ou chaves primárias. A manutenção desses
relacionamentos é realizada por meio de chaves estrangeiras. Por outro lado, na programação orientada a
objetos temos as classes, cujas instâncias comportam valores, e que podem se relacionar com outras
classes por meio de coleções ou atributos. Não existe uma estrutura de indexação, mas uma relação
bilateral, que ocorre por meio de propriedades dos objetos envolvidos.
Temos, então, duas filosofias distintas. Como o ambiente de programação deve gerenciar toda a
lógica, ocorre um esforço natural para minimizar o uso de tabelas e registros, substituindo-os por classes e
objetos. Essa abordagem fica clara quando utilizamos o padrão de desenvolvimento DAO (data access
object), no qual temos uma classe de entidade, e as consultas e operações sobre o banco de dados são
concentradas em uma classe gestora, com a conversão para objetos e coleções, abstraindo o enfoque
relacional no SGBD.
ORM (mapeamento objeto-relacional)
O uso de DAO deu origem à técnica de mapeamento objeto-relacional, ou ORM, na qual uma
entidade (objeto) é preenchida com os dados de um registro (relacional). Esse processo, inicialmente
realizado de maneira programática, foi modificado com o advento de frameworks de persistência, geralmente
baseados no uso de XML (eXtended Markup Language) ou anotações.
Mapeamento objeto-relacional.
Com base nas configurações, que indicam a relação entre atributos da classe e colunas do banco,
bem como chaves e relacionamentos, o framework gera automaticamente todos os comandos SQL
(Structured Query Language) necessários, transmitindo-os para o banco de dados.
Padrões de Projetos de Software com Java
Marcio Quirino - 128
Entity Beans
São parte integrante do J2EE (Java 2 Enterprise Edition) e operam de acordo com o padrão Active
Record, no qual cada operação com um objeto equivale a um comando executado no banco de dados.
Assim, o padrão pode ser ineficiente, devido à grande quantidade de comandos SQL que poderiam ser
executados em blocos.
public abstract class ProdutoEntityBean implements EntityBean {
public abstract int getCodigo();
public abstract void setCodigo(int codigo);
public abstract String getNome();
public abstract void setNome(String nome);
public abstract int getQuantidade();
public abstract void setQuantidade(int quantidade);
// O restante do código foi omitido
}
No fragmento de código, temos o início da definição de um entity bean, em que o objeto é gerado
pelo servidor de aplicativos e as classes de entidade apresentam apenas as propriedades, além de alguns
métodos utilitários. O mapeamento do entity bean para a tabela deve ser feito com base na sintaxe XML,
conforme o exemplo a seguir.
ProdutoEntityBean
PRODUTO
codigo
COD_PRODUTO
nome
NOME
quantidade
QUANTIDADE
Hibernate
Já no framework Hibernate, o padrão DAO é implícito, com os comandos sendo gerados a partir dos
métodos de um gestor de persistência, com base no conjunto de elementos de mapeamento e nos dados
presentes nas entidades.
public class Produto {
private int codigo;
private String nome;
private int quantidade;
public Produto(){}
// Os getters e setters das propriedades foram omitidos
}
Para o Hibernate, as entidades são apenas classes comuns, sem métodos de negócios, com um
conjunto de propriedades e um construtor padrão. O mapeamento é realizado via XML, como no trecho
apresentado a seguir.
Padrões de Projetos de Software com Java
Marcio Quirino - 129
Com o XML, temos um modelo documental para o mapeamento, retirando do código as referências
aos elementos do banco de dados, o que garante maior flexibilidade e menor acoplamento.
Java Persistence API
Além de ser verboso, o uso de XML faz com que alguns problemas surjam apenas no momento da
execução. Um dos maiores avanços do Java foi a criação do JPA, que permitiu padronizar a arquitetura dos
frameworks de persistência e concentrou as configurações no arquivo persistence.xml. Não é apenas uma
biblioteca, mas uma API que define a interface comum, configurável por meio de anotações, que deve ser
seguida pelos frameworks de persistência.
A JPA tem o padrão DAO implícito, o que traz grande eficiência na persistência. Não é por menos
que, na plataforma JEE atual, temos a substituição dos entity beans pelo JPA.
Definindo uma entidade JPA
Para definir uma entidade JPA, devemos criar uma classe sem métodos de negócios, também
conhecida como POJO (plain old java object). A entidade definida deve receber anotações para o
mapeamento entre a classee sua tabela, ou seja, o mapeamento objeto-relacional.
@Entity
@Table(name = "PRODUTO")
@NamedQueries({
@NamedQuery(name = "Produto.findAll",
query = "SELECT p FROM Produto p")})
public class Produto implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Basic(optional = false)
@Column(name = "COD_PRODUTO")
private Integer codigo;
@Column(name = "QUANTIDADE")
private Integer quantidade;
public Produto() {
}
public Produto(Integer codigo) {
this.codigo = codigo;
}
// Os getters e setters das propriedades foram omitidos
@Override
public int hashCode() {
int hash = 0;
hash += (codigo != null ? codigo.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object object) {
if (object==null||!(object instanceof Produto)) {
return false;
}
Produto other = (Produto) object;
return this.codigo!=null &&
this.codigo.equals(other.codigo);
}
@Override
public String toString() {
return "model.Produto[ codigo=" + codigo + " ]";
}
}
A anotação Entity define a classe Produto como uma entidade para o JPA, enquanto Table especifica
a tabela para a qual será mapeada no banco de dados, com base no parâmetro name. Utilizamos ainda a
Padrões de Projetos de Software com Java
Marcio Quirino - 130
anotação NamedQueries para criar consultas por meio de uma sintaxe denominada JPQL (Java Persistence
Query Language).
Vejamos, a seguir, as principais anotações do JPA.
• Entity: marca a classe como uma entidade para o JPA.
• Table: especifica a tabela que será utilizada no mapeamento.
• Column: mapeia o atributo para o campo da tabela.
• Id: especifica o atributo mapeado para a chave primária.
• Basic: define a obrigatoriedade do campo ou o modo utilizado para a carga de dados.
• OneToMany: mapeia a relação 1XN do lado da entidade principal por meio de uma coleção.
• ManyToOne: mapeia a relação 1XN do lado da entidade dependente, com base em uma
classe de entidade.
• OneToOne: mapeia o relacionamento 1X1 com atributos de entidade em ambos os lados.
• ManyToMany: mapeia o relacionamento NXN com atributos de coleção em ambos os lados.
• OrderBy: define a regra que será adotada para ordenar a coleção.
• JoinColumn: especifica a regra de relacionamento da chave estrangeira ao nível das tabelas.
As entidades JPA devem conter um construtor vazio e um outro baseado na chave primária, como
podemos verificar no código de Produto, além dos métodos equals e hashCode. Ambos os métodos
utilitários são baseados no atributo código, que identifica a instância.
Ainda precisamos do atributo serialVersionUID, referente à versão da classe e utilizado nos
processos de migração da base de dados. Por fim, a implementação de toString nos dá controle sobre a
representação da entidade como texto.
Além das anotações nas entidades, precisamos configurar o arquivo persistence.xml, definindo os
aspectos gerais da conexão com o banco de dados. O arquivo deve ser criado na pasta META-INF, e os
parâmetros podem incluir elementos como a classe de conexão JDBC (Java Database Connectivity) ou o
pool de conexões do servidor, sendo sempre presente a especificação do framework de persistência
utilizado.
org.eclipse.persistence.jpa.PersistenceProvider;
model.Produto
A primeira informação relevante é o nome da unidade de persistência (ExemploSimplesJPAPU), com
o tipo de transação que será utilizado. Transações são necessárias para garantir o nível de isolamento
adequado entre tarefas, como no caso de múltiplos usuários acessando o mesmo banco de dados.
Padrões de Projetos de Software com Java
Marcio Quirino - 131
O controle transacional pode ocorrer a partir de um gestor próprio, para uso no ambiente JSE (Java
Standard Edition) ou pelo JEE (Java Enterprise Edition) no modelo não gerenciado, mas também permite o
modo gerenciado, por meio da integração com JTA (Java Transaction API).
Em que temos:
A. RESOURCE_LOCAL
✓ Utiliza o gestor de transações do JPA, para execução no JSE ou no modelo não gerenciado
do JEE.
B. JTA
✓ Ativa a integração com JTA, para utilizar o gerenciamento de transações pelo JEE.
Em seguida, definimos o provedor de persistência no elemento provider. O elemento class define as
classes de entidade e as propriedades da conexão são definidas no grupo properties.
Consulta e manipulação de dados
Com as entidades mapeadas e a conexão configurada, podemos consultar e manipular os dados
utilizando um gestor de entidades (EntityManager).
Entity Manager
Concentra os métodos que invocam os comandos SQL montados pelo JPA a partir das anotações
da entidade, de uma forma totalmente transparente.
public class Principal {
public static void main(String[] args) {
EntityManagerFactory emf =
Persistence.createEntityManagerFactory(
"ExemploSimplesJPAPU");
EntityManager em = emf.createEntityManager();
Query query = em.createNamedQuery("Produto.findAll");
List lista = query.getResultList();
lista.forEach((e) -> {
System.out.println(e.getNome());
});
em.close();
}
}
A. Passo 1
✓ O primeiro passo é a definição do EntityManagerFactory, utilizando o nome da unidade de
persistência (ExemploSimplesJPAPU). Em seguida, obtemos uma instância de
EntityManager a partir da fábrica de gestores, utilizando o método createEntityManager.
B. Passo 2
✓ Com o gestor instanciado, obtemos um objeto do tipo Query, com a chamada para
createNamedQuery, que utiliza uma NamedQuery da classe Produto. As consultas nomeadas
devem apresentar nomes únicos pois, do contrário, poderiam gerar dualidade durante a
execução.
C. Passo 3
✓ O método getResultList retorna o resultado da consulta ao SGBD em um objeto
List. Em termos práticos, a instrução JPQL é transformada em um comando SQL,
que é transmitido para o banco de dados via JDBC, e o resultado da consulta é convertido
em uma coleção de objetos, a partir do mapeamento efetuado com as anotações do JPA. Ao
final, encerramos a comunicação com o banco de dados, utilizando o método close do
EntityManager.
Padrões de Projetos de Software com Java
Marcio Quirino - 132
Atenção!
Note que o JPA não elimina o uso de JDBC, pois gera apenas os comandos SQL de forma automatizada,
utilizando anotações.
Inclusão de dados
Agora, podemos verificar como é feita a inclusão de um produto em nossa base de dados.
public static void incluir(Produto p){
EntityManagerFactory emf =
Persistence.createEntityManagerFactory(
"ExemploSimplesJPAPU");
EntityManager em = emf.createEntityManager();
try {
em.getTransaction().begin();
em.persist(p);
em.getTransaction().commit();
}catch(Exception e){em.getTransaction().rollback();
}finally{
em.close();
}
}
Inclusões podem gerar erros, portanto, o ideal é utilizar transações para executá-las. Na verdade,
qualquer manipulação de dados efetuada a partir do JPA exige uma transação.
Após obtermos uma instância de EntityManager na variável em, é definido um bloco de código
protegido, no qual a transação é iniciada com begin, seguida da inclusão do produto na base de dados por
meio do método persist, e temos a confirmação da transação com o uso de commit.
Caso ocorra um erro, todas as alterações efetuadas são desfeitas com o uso de rollback, e ainda
temos um trecho finally, em que fechamos a comunicação com o uso de close, independentemente da
ocorrência de erros.
Para efetuar a alteração dos dados de um registro, temos um processo muito similar, trocando
apenas o método persist por merge.
public static void alterar(Produto p){
EntityManagerFactory emf =
Persistence.createEntityManagerFactory(
"ExemploSimplesJPAPU");
EntityManager em = emf.createEntityManager();
try {
em.getTransaction().begin();
em.merge(p);
em.getTransaction().commit();
}catch(Exception e){
em.getTransaction().rollback();
}finally{
em.close();
}
}
Exclusão de dados
Para excluir um registro, devemos utilizar o método find para recuperá-lo. A exclusão em si será
executada por meio do método remove, que receberá a instância em questão.
public static void excluir(Integer codigo){
EntityManagerFactory emf =
Persistence.createEntityManagerFactory(
"ExemploSimplesJPAPU");
EntityManager em = emf.createEntityManager();
try {
em.getTransaction().begin();
Padrões de Projetos de Software com Java
Marcio Quirino - 133
em.remove(em.find(Produto.class, codigo));
em.getTransaction().commit();
}catch(Exception e){
em.getTransaction().rollback();
}finally{
em.close();
}
}
Podemos concluir que os métodos find, persist, merge e remove correspondem, respectivamente,
aos comandos SELECT, INSERT, UPDATE e DELETE, ao nível do banco de dados.
Execução do aplicativo
Vamos evitar aqui algumas complexidades do ambiente corporativo, direcionando nosso foco apenas
para a interação com bancos de dados. Para isso, vamos utilizar uma aplicação Java simples.
Veja, a seguir, como criamos a aplicação Java que iremos utilizar.
"Wizard" para criação de projetos.
Neste estudo, trabalharemos com o banco de dados Derby, também chamado de Java DB, exigindo
a inclusão da biblioteca jdbc correspondente.
Adicionando a biblioteca JDBC e o framework EclipseLink
Para adicionar a biblioteca JDBC do Derby, vamos clicar com o botão direito sobre a divisão Libraries,
e escolher a opção Add Library. Na janela seguinte, selecionaremos Java DB Driver e clicaremos no botão
Add Library.
Padrões de Projetos de Software com Java
Marcio Quirino - 134
"Wizard" para inclusão de bibliotecas no projeto do NetBeans.
Precisamos acrescentar o framework JPA escolhido, no caso, o EclipseLink. Veja como fazer o
download desse framework na seção Explore +.
Após efetuar o download da versão mais recente do EclipseLink no formato zip e extrair para algum
diretório de fácil acesso, crie uma biblioteca utilizando a opção de menu Tools.Libraries. Em seguida, clique
em New Library. A essa nova biblioteca daremos o nome EclipseLink2.7, e adicionaremos o arquivo
eclipselink.jar, presente no diretório jlib, além de todos os arquivos no formato jar do subdiretório jpa. Veja
na imagem a seguir.
"Wizard" para inclusão de bibliotecas no projeto do NetBeans.
Após a definição da biblioteca, vamos adicionar ao projeto, da mesma forma que fizemos para o
driver JDBC. Ao final, teremos a configuração de bibliotecas para o projeto conforme a imagem a seguir.
Padrões de Projetos de Software com Java
Marcio Quirino - 135
Navegador do projeto no NetBeans.
Criando o banco de dados Derby
Agora, só precisamos de um banco de dados Derby, que será criado de forma simples, por meio da
aba Services do NetBeans, na divisão Databases. Veja na imagem a seguir.
Navegador de serviços no NetBeans.
Para criarmos um banco de dados, clique com o botão direito sobre o driver Java DB da árvore de
Databases e escolha da opção Create Database. Na janela seguinte, preencha o nome do banco de dados,
o usuário e a senha com o valor bancoJPA. Veja na imagem a seguir.
Padrões de Projetos de Software com Java
Marcio Quirino - 136
"Wizard" para criação de banco de dados derby no NetBeans.
A conexão é aberta com um duplo-clique sobre o identificador do banco de dados. Após conectado,
execute os comandos SQL de criação da tabela, clicando com o botão direito sobre a conexão e escolhendo
a opção Execute Command.
Veremos que a janela de edição de SQL será aberta, permitindo que seja digitado o script
apresentado a seguir. Para executar nosso script, devemos pressionar CTRL+SHIFT+E, ou clicar no botão
de execução de SQL na parte superior do editor.
CREATE TABLE PRODUTO (
COD_PRODUTO INTEGER NOT NULL PRIMARY KEY,
NOME VARCHAR(50),
QUANTIDADE INTEGER);
INSERT INTO PRODUTO VALUES (1,'Morango',200);
INSERT INTO PRODUTO VALUES (2,'Banana',1000);
INSERT INTO PRODUTO VALUES (3,'Manga',600);
SELECT * FROM PRODUTO;
Resultado da execução do aplicativo
Ao rodar o programa, a listagem da tabela com os registros inseridos será apresentada na própria
janela de edição, em uma divisão própria. Agora, podemos executar nosso projeto, gerando a saída
apresentada a seguir.
Saída do programa apresentada no console.
Manipulando dados com NamedQueries
Como já vimos, é extremamente necessário estabelecer a comunicação entre a aplicação e o banco
de dados. Normalmente, precisamos realizar alguma consulta parametrizada, o que inclui receber os
parâmetros, como uma chave primária para identificação, e enviar a instrução para o banco de dados.
Padrões de Projetos de Software com Java
Marcio Quirino - 137
Roteiro de prática
Avançaremos um pouco mais, propondo a você o seguinte desafio: alterar o programa apresentado
anteriormente, de forma que a classe principal.Principal utilize o código para selecionar um produto
específico. Para isto, é importante realizar as etapas a seguir.
• Utilizar uma anotação para criar uma NamedQuery na classe Produto
• Alterar a NamedQuery utilizada em Principal
• Incluir os parâmetros no objeto query
Veja o resultado dessa prática.
Classe “modelo.Produto”:
@Entity
@Table(name = "PRODUTO")
@NamedQueries({
@NamedQuery(name = "Produto.findAll", query = "SELECT p FROM Produto p"),
@NamedQuery(name = "Produto.findByCodProduto", query = "SELECT p FROM Produto p WHERE
p.codProduto = :codProduto")
})
public class Produto implements Serializable {
// código omitido
Classe "principal.Principal":
public class Principal {
public static void main(String[] args) {
EntityManagerFactory emf
= Persistence.createEntityManagerFactory(
"ExemploSimplesJPAPU");
EntityManager em = emf.createEntityManager();
Query query = em.createNamedQuery("Produto.findByCodProduto");
query.setParameter("codProduto", 1);
List lista = query.getResultList();
lista.forEach((e) -> {
System.out.println(e.getCodProduto() + "-" + e.getNome());
});
em.close();
}
}
2. Implementação de regras de negócio com EJBs
Enterprise Java Beans (EJB)
No ambiente Java, a arquitetura de objetos distribuídos geralmente é o elemento central dos
servidores corporativos. Podemos observar a importância dessa arquitetura no uso de componentes EJB
(Enterprise Java Bean)em servidores de aplicação, como o Glassfish.
Os EJB são componentes corporativos utilizados de forma indireta dentro de um ambiente de objetos
distribuídos, suportando elementos da plataforma JEE (Java Enterprise Edition). Todo EJB é executado em
um pool de objetos, cujo número de instâncias varia de acordo com a demanda, segundo um intervalo de
tempo estabelecido.
Podemos acessar os serviços oferecidos pelo pool de EJBs por meio de interface local
(EJBLocalObject) ou remota (EJBObject), gerada a partir de componentes de fábrica, criados com a
implementação de EJBLocalHome, para acesso local, ou EJBHome, para acesso remoto. Como é padrão
na plataforma Java, as fábricas são registradas e localizadas via JNDI (Java Naming and Directory Interface).
O processo para acessar o pool de EJBs envolve três passos:
Padrões de Projetos de Software com Java
Marcio Quirino - 138
A. Passo 1
✓ Acesso à fábrica de interfaces, por meio de JNDI.
B. Passo 2
✓ Geração da interface de acesso pela fábrica.
C. Passo 3
✓ Entrega da interface ao cliente, permitindo iniciar o diálogo com o pool.
Veja a representação desses passos na imagem a seguir.
Servidor e contêineres Java EE.
O acesso ao banco de dados é realizado por meio de um pool de conexões JDBC, representado por
um objeto do tipo DataSource, que é registrado via JNDI. Quando solicitamos uma conexão ao DataSource,
não estamos abrindo uma nova conexão, mas reservando uma das disponíveis no pool. Quando invocamos
o método close, não ocorre a desconexão, mas sim a liberação da conexão para a próxima requisição.
No código a seguir, temos um exemplo de utilização de pool de conexões. Após obter o recurso via
JNDI, por meio do método lookup de InitialContext, efetuamos a conversão para o tipo correto, no caso, um
DataSource que fornece conexões através do método getConnection. O restante da programação é a
mesma de um acesso local ao banco de dados.
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
try (PrintWriter out = response.getWriter()) {
out.println("");
try {
InitialContext ctx = new InitialContext();
DataSource dts = (DataSource) ctx.lookup("jdbc/loja");
Padrões de Projetos de Software com Java
Marcio Quirino - 139
Connection c1 = dts.getConnection();
Statement st = c1.createStatement();
ResultSet rs = st.executeQuery("SELECT * FROM PRODUTO");
while(rs.next())
out.println(rs.getString("NOME")+"
");
c1.close();
} catch (SQLException | NamingException ex) {
}
out.println("");
}
}
O JPA abstrai a localização e utilização do pool, que fica a cargo do framework de persistência. No
fluxo normal de execução, o cliente faz uma solicitação para a interface de acesso, que é repassada para o
pool. O pool disponibiliza um EJB para responder à solicitação. Já na programação do EJB, utilizamos o
JPA para obter acesso ao banco de dados.
A programação, no modelo adotado a partir do JEE5, é bastante simples, e precisaremos apenas
das anotações corretas para que os application servers, como o JBoss ou o GlassFish, se encarreguem de
montar toda a estrutura necessária. Isso é bem diferente do processo de criação adotado pelo J2EE,
envolvendo uma grande quantidade de interfaces, classes e arquivos XML, com a verificação sendo
realizada apenas no momento da implantação do sistema.
Session beans
Esses componentes fundamentais no desenvolvimento de aplicações empresariais em Java são
responsáveis por encapsular a lógica de negócios e fornecer serviços específicos para os clientes. Os
session beans são amplamente utilizados para implementar transações, acesso a bancos de dados e outras
operações relacionadas à lógica de negócios.
Os EJBs são utilizados para implementar as regras de negócio do nosso aplicativo com base nas
entidades e nos requisitos definidos. Esses componentes não podem acumular a responsabilidade sobre a
estrutura de persistência utilizada. Também devemos observar que as regras de negócio devem ser
totalmente independentes das interfaces do sistema.
O primeiro tipo de EJB que deve ser observado é o de sessão (session beans), responsável por
efetuar processos de negócios de forma síncrona, e configurável, podendo apresentar três comportamentos
distintos:
A. Stateless
✓ Não permite a manutenção de estado, ou seja, não guarda valores entre chamadas
sucessivas.
B. Stateful
✓ É utilizado quando é necessário manter valores entre chamadas sucessivas, como no caso
de somatórios.
C. Singleton
✓ Permite apenas uma instância por máquina virtual, garantindo o compartilhamento de dados
entre todos os usuários.
Stateless e stateful
Utilizamos stateless quando não precisamos de informações dos processos anteriores ao corrente.
Qualquer instância do pool de EJBs pode ser escolhida, e não é necessário efetuar a carga de dados
anteriores, definindo o padrão de comportamento mais ágil para um session bean.
O comportamento stateful deve ser utilizado apenas quando precisamos de informações anteriores,
como em uma cesta de compras virtual, ou processos com acumuladores em cálculos estatísticos, entre
outras situações com necessidade de gerência de estados.
Padrões de Projetos de Software com Java
Marcio Quirino - 140
Interface de acesso
Antes de definir um session bean, devemos definir sua interface de acesso, com base na anotação
Local, para acesso interno, ao nível do servidor, ou Remote, permitindo que o componente seja acessado
remotamente. Em nossos estudos, será suficiente o uso de acesso local, já que teremos o acionamento dos
EJBs a partir dos servlets.
@Local
public interface CalculadoraLocal {
int somar(int a, int b);
}
Ao criarmos o EJB, ele deverá implementar a interface de acesso, além de ser anotado como
stateless ou stateful, dependendo da necessidade do negócio. Para uma calculadora simples, não
precisaríamos de gerência de estados.
@Stateless
public class Calculadora implements CalculadoraLocal {
@Override
public int somar(int a, int b) {
return a + b;
}
}
Singleton
O session bean que implementa o Singleton é utilizado para compartilhar dados entre os usuários
conectados, mesmo na execução em ambientes distribuídos. É importante observar que a tecnologia de
EJBs é empregada em sistemas de missão crítica, que costumam trabalhar com clusters de computadores.
Session bean com Servlet
O código a seguir representa o processo de utilização de um session bean a partir de um Servlet.
@WebServlet(name = "ServletSoma",
urlPatterns = {"/ServletSoma"})
public class ServletSoma extends HttpServlet {
@EJB
CalculadoraLocal facade;
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
try (PrintWriter out = response.getWriter()) {
out.println("");
out.println("Servlet ServletSoma: " +
facade.somar(2, 3) + "");
out.println("");
out.println("");
}
}
}
Tudo que precisamos fazer é anotar um atributo, do tipo da interface local, com EJB. No exemplo,
temos a interface CalculadorLocal referenciada no atributo facade, o que permite invocar o método somar,
sendo executado pelo pool.
Quase todos os processos de negócio de um sistema corporativo são implementados pormeio de
session beans. Contudo, alguns comportamentos não podem ser definidos de forma síncrona, exigindo
comunicação com mensagerias, seguindo um modelo assíncrono.
Padrões de Projetos de Software com Java
Marcio Quirino - 141
Message-driven beans (MDB)
Protocolos de mensageria desempenham um papel fundamental em sistemas Java. Esses
protocolos definem conjuntos de regras e formatos utilizados para facilitar a troca de mensagens entre
diferentes sistemas. Eles determinam como as mensagens são enviadas, recebidas, processadas e
entregues aos MDBs (message-driven Beans). Esses protocolos oferecem recursos como persistência de
mensagens, filas de mensagens e garantia de entrega, possibilitando uma comunicação confiável e
assíncrona para os MDBs.
Mensagerias
As mensagerias atuam de forma assíncrona e podem ser:
Modelo publish/subscribe
As mensagens são depositadas em tópicos para que os assinantes as recuperem.
Modelo point to point
As mensagens são enfileiradas para um tratamento sequencial no destinatário.
O uso de mensagerias permite a construção de sistemas B2B quase sem acoplamento, em que o
único elemento de ligação entre os sistemas das duas empresas é a mensagem transmitida entre elas.
Após o emissor publicar uma mensagem, a gerência passa a ser da mensageria, até a retirada pelos
receptores. Mesmo que o receptor esteja inativo, as mensagens não se perdem, sendo acumuladas até o
momento em que o receptor seja ativado.
Para criar filas ou tópicos no GlassFish, é necessário utilizar o comando asadmin, como no exemplo
seguinte, para a criação de uma fila denominada jms/SimpleQueue.
asadmin create-jms-resource –restype javax.jms.ConnectionFactory
jms/SimpleConnectionFactory
asadmin create-jms-resource --restype javas.jms.Queue jms/SimpleQueue
Padrões de Projetos de Software com Java
Marcio Quirino - 142
Também podemos abrir o console do asadmin sem a passagem de parâmetros e invocar os
comandos internamente. Vejamos.
Comandos executados no shell do asadmin.
MDB (message-driven bean)
Existe um tipo de EJB denominado MDB (message-driven bean), que tem como finalidade a
comunicação com mensagerias via JMS (Java Message Service), possibilitando o processamento
assíncrono no JEE. Com o MDB, é possível trabalhar nos dois domínios de mensagerias, com o tratamento
via pool de EJBs, a partir do método onMessage, representando os eventos de recepção de mensagens.
@MessageDriven(activationConfig = {
@ActivationConfigProperty(propertyName = "destinationLookup",
propertyValue = "jms/SimpleQueue"),
@ActivationConfigProperty(propertyName = "destinationType",
propertyValue = "javax.jms.Queue")
})
public class Mensageiro001 implements MessageListener {
public Mensageiro001() {
}
@Override
public void onMessage(Message message) {
try {
System.out.println("Mensagem enviada: "+
((TextMessage) message).getText());
} catch (JMSException ex) {
System.out.println("Erro: "+ex.getMessage());
}
}
}
No código de exemplo, o MDB é definido utilizando a anotação MessageDriven, com a configuração
para acesso a jms/SimpleQueue, do tipo javax.jms.Queue, por meio de anotações ActivationConfigProperty.
Também é possível utilizar canais internos do projeto, mas o uso de canais do servidor viabiliza o
comportamento B2B, com acesso a partir de qualquer plataforma que dê suporte ao uso de mensagerias.
Para o tratamento das mensagens, devemos implementar a interface MessageListener, que contém
apenas o método onMessage. A mensagem é recebida no parâmetro do tipo Message, sendo necessário
converter para o tipo correto, como no exemplo, em que temos a captura de um texto enviado via
TextMessage, e a impressão da mensagem no console do GlassFish.
Saiba mais
O MDB foi projetado exclusivamente para receber mensagens a partir de filas ou tópicos, o que faz com que
não possa ser acionado diretamente, como os session beans. Para sua ativação, basta que um cliente poste uma
mensagem.
Padrões de Projetos de Software com Java
Marcio Quirino - 143
O código é feito da seguinte forma:
@WebServlet(name = "ServletMessage",
urlPatterns = {"/ServletMessage"})
public class ServletMessage extends HttpServlet {
@Resource(mappedName = "jms/SimpleConnectionFactory")
private ConnectionFactory connectionFactory;
@Resource(mappedName = "jms/SimpleQueue")
private Queue queue;
public void putMessage() throws ServletException {
try {
Connection connection =
connectionFactory.createConnection();
Session session =
connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
MessageProducer messageProducer =
session.createProducer(queue);
TextMessage message = session.createTextMessage();
message.setText("Teste com MDB");
messageProducer.send(message);
} catch (JMSException ex) {
throw new ServletException(ex);
}
}
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
try (PrintWriter out = response.getWriter()) {
out.println("");
out.println("");
putMessage();
out.println("Mensagem Enviada");
out.println("");
}
}
}
O processo é um pouco mais complexo que o adotado para os session beans, mas apresenta menor
acoplamento. Veja o passo a passo:
A. Passo 1
✓ Mapear a fábrica de conexões da mensageria e a fila de destino do MDB, por meio de
anotações Resource.
B. Passo 2
✓ Definir o método putMessage para envio da mensagem, criando uma conexão a partir da
fábrica, uma sessão a partir da conexão, e o produtor de mensagens (MessageProducer) a
partir da sessão.
C. Passo 3
✓ Criar a mensagem de texto (TextMessage) por meio da sessão, definir o texto que será
enviado com setText, e finalmente enviar a mensagem, finalizando a definição do método
putMessage.
Veja a representação na imagem a seguir:
Padrões de Projetos de Software com Java
Marcio Quirino - 144
Console do Glassfish exibindo as mensagens de teste.
Aplicativo corporativo
Aplicativos Java corporativos são softwares desenvolvidos para atender às necessidades complexas
e específicas das organizações. Eles são construídos utilizando tecnologias Java Enterprise Edition (Java
EE), fornecendo recursos avançados, como gerenciamento de transações, segurança, escalabilidade e
integração com sistemas legados. Esses aplicativos são projetados para lidar com grandes volumes de
dados e suportar operações críticas para o funcionamento das empresas.
EJBs
Para que possamos trabalhar com EJBs, por meio do ambiente do NetBeans, devemos definir um
projeto corporativo. A sequência de passos para criar o aplicativo corporativo pode ser observada a seguir.
A. Passo 1
✓ Criar um projeto do tipo Enterprise Application, na categoria Java Enterprise.
B. Passo 2
✓ Preencher o nome (ExamploEJB) e local do projeto.
Padrões de Projetos de Software com Java
Marcio Quirino - 145
C. Passo 3
✓ Escolher o servidor (GlassFish) e versão do JEE (Java EE7), além de marcar as opções de
criação para os módulos EJB e web.
Seguindo corretamente os passos apresentados, você vara que serão gerados três projetos.
Poderemos visualizá-los na interface do NetBeans.
A seguir, apresentamos as características de cada um dos projetos.
Padrões de Projetos de Software com Java
Marcio Quirino - 146
1. ExemploEJB-ejb
✓ Utilizado na definiçãodas entidades JPA e dos componentes EJB, sendo compilado com a
extensão "jar".
2. ExemploEJB-war
✓ Contém os elementos para web, como servlets, facelets e páginas XHTML, compilados para
um arquivo "war".
3. ExemploEJB
✓ Agrupa os dois projetos anteriores, compactados em apenas um arquivo, com adoção da
extensão "ear", para implantação.
Quando trabalhamos com um projeto corporativo, devemos implantar o projeto principal, com
extensão ear (enterprise archived), cujo ícone é um triângulo, pois qualquer tentativa de implantar os dois
projetos secundários irá impedir a execução correta do conjunto, exigindo que seja feita a remoção manual
dos projetos anteriores pela aba de serviços.
Remoção de aplicação web do servidor Glassfish.
Agora, vamos criar nosso primeiro session bean, configurado como stateless, no projeto secundário
ExemploEJB-ejb, adicionando um novo arquivo e seguindo alguns passos:
A. Passo 1
✓ Selecionar o tipo de session bean na categoria Enterprise Java Beans.
B. Passo 2
✓ Definir o nome (Calculadora) e pacote (ejbs) do novo session bean, escolher o tipo como
Stateless e marcar apenas a interface Local
Padrões de Projetos de Software com Java
Marcio Quirino - 147
Em diversas situações, a IDE mostra um erro de compilação decorrente da importação dos
componentes da biblioteca javax.ejb. Caso esse problema ocorra, a solução é simples com a inclusão da
biblioteca Java EE 7 API no projeto.
Após incluir as bibliotecas necessárias, podemos completar os códigos de Calculadora e
CalculadoraLocal, de acordo com os exemplos apresentados anteriormente, e iremos testar o EJB, ainda
seguindo os exemplos, por meio de um servlet. Como os componentes web são criados ao nível de
ExemploEJB-war, devemos acessar o projeto e adicionar um novo arquivo do tipo servlet, na categoria web,
com o nome ServletSoma e pacote servlets, sem adicionar informações ao arquivo web.xml.
Com o componente ServletSoma criado, utilizamos o código de exemplo definido antes, com a
chamada para o EJB, executamos o projeto principal (ExemploEJB), e efetuamos a chamada apropriada:
http://localhost:8080/ExemploSimplesJPA-war/ServletSoma.
http://localhost:8080/ExemploSimplesJPA-war/ServletSoma
Padrões de Projetos de Software com Java
Marcio Quirino - 148
MDBs
Para criar o EJB do tipo MDB, devemos adicionar, no projeto ExemploEJB-ejb, um novo arquivo do
tipo message-driven bean, na categoria Enterprise Java Beans.
Precisamos definir o nome (Mensageiro001) e o pacote do componente, além de escolher a fila para
as mensagens no servidor (jms/SimpleQueue), como na imagem a seguir.
Todos os passos para a codificação de Mensageiro001 foram descritos anteriormente, bem como a
do servlet para postagem da mensagem, com o nome ServletMessage, mas lembre-se de que o servlet deve
ser criado no projeto ExemploEJB-war. Com todos os componentes implementados, basta implantar o
sistema, a partir do projeto principal, e efetuar a chamada correta no navegador pelo endereço:
"http://localhost:8080/ExemploSimplesJPA-war/ServletMessage".
Após efetuar a chamada, você verá a página com a informação de que ocorreu o envio da mensagem,
o que poderá ser verificado no console de saída do GlassFish, conforme descrito anteriormente.
Aplicando os MDBs
No mundo web é bastante comum a utilização de recursos externos, sejam eles de outro projeto da
sua organização ou mesmo de outra organização. Esses recursos podem ser desenvolvidos utilizando
linguagens e respeitando diferentes padrões. Para facilitar a comunicação, uma excelente alternativa é a
troca de mensagens.
Roteiro de prática
No mundo Java, utilizamos muito os MDB (message-driven beans). Portanto, vamos exercitar a
criação desse tipo de componente desenvolvendo duas classes no mesmo projeto: uma para receber
mensagens e outra para produzir. Para isso, vamos usar o padrão point-to-point, conforme as etapas a
seguir:
• Crie a fábrica de conexões e a fila no glassfish, utilizando o asadmin.
• No projeto ejb, desenvolva a classe MeuReceptor, responsável por tratar as mensagens.
• No projeto war, desenvolva uma classe MeuProdutor, que contenha um servlet para enviar
uma mensagem para o receptor.
• No projeto war, altere o index.html com um formulário de envio de mensagem.
• Verifique o resultado no console do glassfish.
Veja o resultado dessa prática.
Padrões de Projetos de Software com Java
Marcio Quirino - 149
"MeuReceptor.java"
@MessageDriven(activationConfig = {
@ActivationConfigProperty(propertyName = "destinationLookup", propertyValue =
"jms/MinhaQueue"),
@ActivationConfigProperty(propertyName = "destinationType", propertyValue =
"javax.jms.Queue")
})
public class MeuReceptor implements MessageListener {
public MeuReceptor() {
}
@Override
public void onMessage(Message message) {
try { System.out.println("Mensagem enviada: "
+ ((TextMessage) message).getText());
} catch (JMSException ex) {
System.out.println("Erro: " + ex.getMessage());
}
}
}
"MeuProdutor.java"
@WebServlet("/MeuProdutor")
public class MeuProdutor extends HttpServlet {
@Resource(mappedName = "jms/MinhaConnectionFactory")
private ConnectionFactory connectionFactory;
@Resource(mappedName = "jms/MinhaQueue")
private Queue queue;
public void putMessage(String mensagem) {
try {
Connection connection = connectionFactory.createConnection();
Session session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
MessageProducer messageProducer
= session.createProducer(queue);
TextMessage message = session.createTextMessage();
message.setText(mensagem);
messageProducer.send(message);
} catch (JMSException ex) {
}
}
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws IOException{
response.setContentType("text/html;charset=UTF-8");
request.setCharacterEncoding("UTF-8");
String mensagem = request.getParameter("mensagem");
putMessage(mensagem);
PrintWriter out = response.getWriter();
out.println("Sua Mensagem foi Enviada");
out.println("Mensagem: " + mensagem);
}
}
"Index.html"
TODO supply a title
Padrões de Projetos de Software com Java
Marcio Quirino - 150
3. Arquitetura MVC no Java
Padrões de desenvolvimento
A orientação a objetos representou um grande avanço na implementação de sistemas, pois
aproximou a modelagem da codificação. Nesse contexto, os padrões de desenvolvimento fornecem ainda
mais vantagens ao desenvolvimento, definindo soluções reutilizáveis, com nome, descrição da finalidade,
modelagem UML e modo de utilização.
Entre os diversos padrões de projeto, alguns se destacam nos sistemas corporativos, como Facade,
Proxy, Flyweight, Front Controller e DAO (data access object). Na tecnologia JPA observamos o padrão
DAO, com comandos para acesso ao banco de dados agrupados em classes específicas, separadas do
restante do sistema.
Descrição dos padrões de desenvolvimento
Vejamos uma descrição formal dos padrões de desenvolvimento citados, além de alguns outros,
comuns em sistemas criados para o ambiente JEE.
• Abstract Factory: definição de uma arquitetura abstrata para a geração de objetos, muito
comum em frameworks.
• Command: encapsula o processamentoda resposta para algum tipo de requisição, muito
utilizado para o tratamento de solicitações feitas no protocolo HTTP.
• Data Access Object: utiliza classes específicas para concentrar as chamadas para o banco
de dados.
• Facade: encapsula as chamadas para um sistema complexo, muito utilizado em ambientes
corporativos.
• Flyweight: cria grupos de objetos que respondem a uma grande quantidade de chamadas.
• Front Controller: concentra as chamadas para o sistema, efetuando os direcionamentos
corretos para cada chamada.
• Iterator: permite acesso sequencial aos objetos de uma coleção, o que é implementado
nativamente no Java.
• Proxy: Define um objeto para substituir a referência de outro, utilizado nos objetos remotos
para deixar a conexão transparente para o programador.
• Service Locator: gerencia a localização de recursos compartilhados, com base em serviços
de nomes e diretórios.
• Singleton: garante a existência de apenas uma instância para a classe, como em controles
de acesso.
• Strategy: seleciona algoritmos em tempo de execução, com base em algum parâmetro
fornecido.
Atualmente, os sistemas combinam diversos padrões de projeto em suas implementações.
Exemplo
Criação de um pool de processadores de resposta para solicitações de usuários remotos, o que poderia ser
caracterizado por um Flyweight de Strategies para a escolha de Commands, além da utilização de Proxy na
comunicação com os clientes.
Padrões de Projetos de Software com Java
Marcio Quirino - 151
Padrões arquiteturais
A arquitetura de um sistema define sua estrutura de alto nível, formalizando a organização em termos
de componentes e interações. Um dos objetivos é aproximar a visão de projeto da implementação do
sistema, impedindo que ocorra a previsão de funcionalidades inviáveis para a fase de codificação.
As especificações da arquitetura também definem as interfaces de entrada ou saída de informações,
forma de comunicação com outros sistemas, e regras para o agrupamento dos componentes com base em
suas áreas de atuação, entre outras características. Entenda:
A. Modelo arquitetural
✓ Define a arquitetura de forma abstrata, com foco apenas no objetivo ou característica
principal.
B. Padrão arquitetural
✓ Define o perfil dos componentes estruturais, o modelo de comunicação e até os padrões de
desenvolvimento mais adequados.
Por exemplo, o modelo de objetos distribuídos define apenas atributos essenciais para delimitar um
ambiente com coleções de objetos respondendo a requisições.
Existem diferentes definições de modelos para os padrões arquiteturais, e alguns deles satisfazem a
mais de um modelo. Vejamos.
Padrão arquitetural Modelo(s)
Broker Sistemas Distribuídos
Camadas Mud to structure, chamada e retorno
Orientado a objetos Chamada e retorno
Programa principal e sub-rotina Chamada e retorno
Pipes/Filters Mud to structure, fluxo de dados
Blackboard Mud to structure, centrada em dados
Lote Fluxo de dados
Repositório Centrada em dados
Processos comunicantes Componentes independentes
Event-Driven Componentes independentes
Interpretador Máquina virtual
Baseado em regras Máquina virtual
MVC Sistemas interativos
PAC Sistemas interativos
Microkernel Sistemas adaptáveis
Reflexiva Sistemas adaptáveis
A arquitetura de referência mapeia um padrão arquitetural para componentes de software, que são
capazes de implementar as funcionalidades requeridas de forma cooperativa. Definida a referência,
finalmente pode ser construída uma arquitetura, com todos os componentes adequadamente codificados.
Um exemplo de arquitetura é o VisiBroker, da fabricante Inprise, que utiliza como referência o CORBA
(common object request broker architecture), baseado no modelo de objetos distribuídos, segundo o padrão
arquitetural Broker. Veja essa representação na imagem a seguir.
Padrões de Projetos de Software com Java
Marcio Quirino - 152
Para utilizar uma arquitetura, é necessário compreender sua finalidade, de acordo com o padrão
arquitetural adotado. Veja:
A. Sistemas de baixa complexidade
✓ Podem basear a arquitetura nos paradigmas adotados para a codificação, como a orientação
a objetos.
B. Sistemas de alta complexidade
✓ Exigem uma padronização mais robusta, incluindo a forma de comunicação em rede, gerência
de pools de objetos, entre outros aspectos.
Ambientes de execução remota, como RPC (remote procedure call) e Web Services, são baseados
em arquiteturas no padrão de processos comunicantes. Nesses ambientes, servidores e clientes podem ser
criados utilizando plataformas de desenvolvimento distintas, e o único fator de acoplamento é o protocolo
de comunicação adotado.
É comum o uso de mensagerias nos sistemas corporativos, nas quais utilizamos o padrão arquitetural
event-driven, baseado na ativação de processos de forma indireta, a partir de mensagens. O papel das
mensagerias é tão importante que o componente adotado na comunicação é chamado de MOM (message-
oriented middleware). As vantagens no uso desse padrão são o acoplamento quase nulo e o processamento
assíncrono.
Um sistema simples pode responder a um único padrão arquitetural, mas os sistemas corporativos
são complexos e heterogêneos, sendo muito comum a adoção de múltiplos padrões combinados.
Normalmente, é adotado um padrão principal, que, no caso dos sistemas cadastrais, é o MVC.
Arquitetura MVC
A arquitetura MVC (model-view-controller) divide o sistema em três camadas, com responsabilidades
específicas.
1. Model: nessa camada, temos as entidades e as classes para acesso ao banco de dados.
2. Controller: aqui, concentramos os objetos de negócio.
3. View: nessa camada, são definidas as interfaces do sistema com o usuário ou com outros
sistemas.
Camadas da MVC
A seguir, vamos ver as principais características de cada camada que compõe a arquitetura MVC.
A. Model (modelo)
✓ Controla toda a persistência do sistema.
✓ Concentra as chamadas ao banco de dados.
✓ Encapsula o estado do sistema.
✓ Pode utilizar mapeamento objeto-relacional.
Padrão DAO é aplicável.
Padrões de Projetos de Software com Java
Marcio Quirino - 153
B. Controller (controlador)
✓ Implementa as regras de negócio do sistema.
✓ Solicita os dados à camada Model.
✓ Não pode ser direcionada para uma interface.
✓ Pode utilizar objetos distribuídos.
✓ Padrão Facade facilita a utilização da camada.
C. View (visualização)
✓ Define a interface do sistema.
✓ Faz requisições para a camada Controller.
✓ Contém apenas regras de formatação.
✓ Podem ser definidas múltiplas interfaces.
✓ Não pode acessar a camada Model.
Uma regra fundamental para a arquitetura MVC é a de que os elementos da camada View não podem
acessar a camada Model. Somente os objetos de negócio da camada Controller podem acessar os
componentes da model, e os elementos da View devem fazer suas requisições exclusivamente para os
objetos de negócio.
A arquitetura MVC é baseada em camadas, e cada camada enxerga apenas a camada
imediatamente abaixo.
Em uma arquitetura MVC, as entidades são as unidades de informação para o trânsito entre as
camadas. Todos os comandos SQL ficam concentrados nas classes DAO. Como apenas a camada
Controller pode acessar a Model, e nela estão as classes DAO, garantimos que as interfaces não acessem
o banco de dados diretamente.
Como as instruções SQL são bastante padronizadas, é possível criar ferramentas para a geração
dos comandos e preenchimento das entidades, bastando expressar a relação entre atributos da entidade e
campos do registro.
A camada Controller precisa ser definida de maneira independente de ambiente específico, como
interfaces SWING ou protocolo HTTP. A única dependência aceitável para os objetos de negócio deve ser
com relação à camada Model. Como são os modelos que gerenciam os componentes DAO, isso diminui a
complexidade nas atividades cadastrais iniciadas148
Aplicando os MDBs ................................................................................................................................ 148
Roteiro de prática ............................................................................................................................... 148
3. Arquitetura MVC no Java ..................................................................................................... 150
Padrões de desenvolvimento ................................................................................................................. 150
Descrição dos padrões de desenvolvimento ...................................................................................... 150
Padrões arquiteturais ............................................................................................................................. 151
Arquitetura MVC ..................................................................................................................................... 152
Camadas da MVC .............................................................................................................................. 152
Componentes Java para MVC ............................................................................................................... 153
Implementação de MVC no ambiente Java ............................................................................................ 156
Roteiro de prática ............................................................................................................................... 156
4. Padrão Front Controller em sistemas MVC ......................................................................... 158
Padrão Front Controller .......................................................................................................................... 158
Camadas Model e Controller .................................................................................................................. 160
Criação da camada Model.................................................................................................................. 160
Criação da camada Controller ............................................................................................................ 163
Padrões de Projetos de Software com Java
Marcio Quirino - 7
Construção dos session beans .......................................................................................................... 165
Camada View ......................................................................................................................................... 168
Implementação do Front Controller ........................................................................................................ 170
Aplicação MVC com padrão Front Controller ......................................................................................... 174
Roteiro de prática ............................................................................................................................... 174
Explore + ................................................................................................................................................ 176
Referências ............................................................................................................................................ 176
Padrões de Projetos de Software com Java
Marcio Quirino - 8
Padrões de Projetos de Software com
Java
Caminho do Brilho: Saiba Quais Conteúdos Você Irá
Aprender Conosco
Onboarding
Sejam bem-vindos à disciplina que é uma chave mestra para o seu sucesso no universo da
Tecnologia da Informação: Padrões de Projetos de Software com Java. Aqui, estamos não só partilhando
conhecimento, mas construindo juntos o alicerce de sua futura carreira.
Esta disciplina é uma jornada enriquecedora, desenhada para desenvolver competências cruciais
que unem teoria e prática, formação acadêmica e aplicação no mercado de trabalho. Vocês, alunos, estão
no limiar de uma transformação significativa, aprendendo a engajar-se com padrões de projeto de software
que são fundamentais na criação de sistemas robustos e eficientes.
Os Padrões GOF, ou Gang of Four, são o coração do design de software orientado a objetos, e o
domínio deles significa compreender a essência de soluções eficazes e elegantes no desenvolvimento de
software. Já os Padrões GRASP trazem os princípios básicos para atribuir responsabilidades em objetos,
fornecendo uma fundação sólida para o bom design de software.
Além disso, com as tecnologias JPA e JEE, vocês mergulharão nos meandros do desenvolvimento
Java para aplicações empresariais, o que representa uma habilidade altamente valorizada no mercado.
Tudo isso está aqui para que vocês possam não apenas aspirar a melhorar suas condições de trabalho,
mas efetivamente alcançá-las.
O aprendizado nesta disciplina é contínuo e dinâmico, assim como o campo da Tecnologia da
Informação. Cada aula, cada exercício prático, cada padrão que vocês dominarem será um degrau a mais
na sua jornada para se tornarem profissionais qualificados e diferenciados. Vocês estão se preparando para
serem líderes em um campo que está em constante evolução e que precisa de mentes aguçadas e criativas.
Lembrando que estamos aqui não apenas para ensinar, mas para provocar e inspirar vocês a verem
além. Queremos que percebam como o conhecimento adquirido é aplicável e vital para o sucesso
profissional. Cada desafio superado aqui é uma pequena vitória, cada projeto concluído é um motivo de
orgulho e cada conceito entendido é uma ferramenta poderosa em seu arsenal profissional.
Celebrem cada conquista e saibam que cada novo padrão aprendido não é apenas mais um tópico
coberto, mas um passo para se tornarem profissionais competentes e inovadores. Vocês são especiais por
estar aqui, desfrutando do privilégio de aprender e crescer em um ambiente universitário. Sejam felizes
nessa jornada de aprendizado e valorizem as transformações que o saber traz.
Ao final desta disciplina, vocês estarão não só prontos para enfrentar os desafios da área de gestão
com competência técnica, mas também para inspirar e educar outros, perpetuando a cadeia de inovação e
excelência na área de TI.
Bons estudos e que este seja o início de um caminho profissional repleto de realizações!
Padrões de Projetos de Software com Java
Marcio Quirino - 9
Padrões GoF de Criação
Descrição
Padrões GoF de Projeto do grupo Criação: Template Method, Abstract Factory, Builder, Prototype e
Singleton.
Propósito
Compreender os padrões de projeto ligados à criação de objetos em projetos orientados a objetos e
identificar oportunidades para a sua aplicação são habilidades importantes para um projetista de software,
pois, sem elas, as soluções geradas podem ser inflexíveis e dificultar a evolução de sistemas de software
em prazo e custo aceitáveis.
Preparação
Antes de iniciar o conteúdo, é recomendado instalar em seu computador um programa que lhe
permita elaborar modelos sob a forma de diagramas da UML (Linguagem Unificada de Modelagem). Nossa
sugestão inicial é o Free Student License for Astah UML, usado nos exemplos deste estudo. Para isso, será
necessário usar seu e-mail institucional para ativar a licença.
Preencha os dados do formulário no site do software, envie e aguarde a liberação de sua licença em
seu e-mail institucional. Ao receber a licença, siga as instruções do e-mail e instale o produto em seu
computador. Os arquivos Astah com diagramas UML utilizados nesse conteúdo estão disponíveis para
download.
Sugestões de links adicionais de ferramentas livres para modelagem de sistemas em UML (UML
Tools) podem ser encontradas em buscas na Internet.
Além disso, recomendamos a instalação de um ambiente de programação em Java. O ambiente
recomendado para iniciantesna View.
Da mesma forma, a camada Controller é o melhor local para definir as regras de autorização para o
uso de funcionalidades do sistema, tendo como base o perfil de um usuário autenticado. Com relação à
autenticação, ela pode ser iniciada por uma tela de login na camada View, com a efetivação na camada
Controller. Nos modelos atuais, é comum a geração de um token, mantendo a independência entre as
camadas.
Componentes Java para MVC
Uma grande vantagem do MVC é o direcionamento do desenvolvedor e das ferramentas para as
necessidades de cada camada. Com a divisão funcional, foram criados diversos frameworks, como o JSF
(Java Server Faces), que define interfaces web na camada View, Spring ou EJB, para implementar as regras
de negócio da camada Controller, e Hibernate, para a persistência ao nível da camada Model.
O uso de camadas especializadas permite a divisão da equipe entre profissionais cujo perfil seja
voltado para criação visual, negócios ou banco de dados. Veja na imagem:
Padrões de Projetos de Software com Java
Marcio Quirino - 154
Os frameworks facilitam a manutenção e evolução do sistema, pois tendem a acompanhar as
tecnologias que surgem ao longo do tempo, mas apenas empresas de grande porte e comunidades de
código aberto são capazes de garantir as atualizações, não sendo raros os casos em que uma ferramenta
menos conhecida é descontinuada.
Em nosso contexto, a camada Model utiliza JPA, e como deve ser utilizada apenas pela camada
Controller, é definida no mesmo projeto em que estão os componentes do tipo EJB. Note que a camada
Controller oferece apenas as interfaces para os EJBs, com os dados sendo transitados na forma de
entidades, sem acesso ao banco de dados, já que anotações não são serializáveis.
Com a abordagem adotada, definimos o núcleo funcional e lógico de nosso sistema, sem a
preocupação de satisfazer a qualquer tipo de tecnologia para construção de interfaces de usuário. A
independência do núcleo garante que ele possa ser utilizado por diversas interfaces simultâneas, como:
• SWING
• HTTP
• Web Services
Padrões de Projetos de Software com Java
Marcio Quirino - 155
Sem que ocorra qualquer modificação nos componentes do tipo JPA ou EJB.
Nos sistemas Java para web, um erro comum é definir os controladores no formato de servlets, pois
as regras de negócio se confundem com as rotinas de conversão utilizadas entre o protocolo HTTP e as
estruturas da linguagem Java. Essa abordagem errônea faz com que qualquer nova interface, como SWING,
Web Services, ou até uma linha de comando, seja obrigada a solicitar os serviços utilizando o protocolo
HTTP, algo que, de forma geral, não é uma exigência das regras de negócio dos sistemas.
Considere que a entidade Produto, definida anteriormente, com uso de tecnologia JPA, seja criada
no projeto ExemploEJB-ejb, no qual codificamos nosso session bean de teste com o nome Calculadora.
Com a presença da entidade no projeto, podemos adicionar outro session bean do tipo Stateless, com o
nome ProdutoGestor e uso de interface Local, para as operações cadastrais.
@Local
public interface ProdutoGestorLocal {
List obterTodos();
void incluir(Produto p);
}
@Stateless
public class ProdutoGestor implements ProdutoGestorLocal {
@Override
public List obterTodos() {
EntityManagerFactory emf = Persistence.
createEntityManagerFactory("ExemploSimplesJPAPU");
EntityManager em = emf.createEntityManager();
Query query = em.createNamedQuery("Produto.findAll");
List lista = query.getResultList();
em.close();
return lista;
}
@Override
public void incluir(Produto p) {
EntityManagerFactory emf = Persistence.
createEntityManagerFactory("ExemploSimplesJPAPU");
EntityManager em = emf.createEntityManager();
try {
em.getTransaction().begin();
em.persist(p);
em.getTransaction().commit();
} catch (Exception e) {
em.getTransaction().rollback();
} finally {
em.close();
}
}
Repare que nossos códigos de exemplos anteriores foram aproveitados aqui, com leves adaptações,
para que as operações sejam disponibilizadas por meio do session bean.
Precisamos adicionar o arquivo persistence.xml, definido em nosso exemplo de JPA, ao diretório
conf do projeto ExemploEJB-ejb, sem modificações, o que levará à utilização de controle transacional de
forma local.
Com nossas camadas Model e Controller completamente codificadas, podemos definir um Servlet,
no projeto ExemploEJB-war, com o nome ServletListaProduto, que será parte da camada View do sistema,
no modelo web. O objetivo do novo componente será a exibição da listagem dos produtos presentes na base
de dados.
@WebServlet(name = "ServletListaProduto",
urlPatterns = {"/ServletListaProduto"})
public class ServletListaProduto extends HttpServlet {
@EJB
ProdutoGestorLocal facade;
Padrões de Projetos de Software com Java
Marcio Quirino - 156
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
try (PrintWriter out = response.getWriter()) {
out.println("");
facade.obterTodos().forEach(p -> {
out.println("
" + p.getNome());
});
out.println("");
}
}
}
No código, temos o atributo facade, do tipo ProdutoGestorLocal, utilizando a anotação EJB para
injetar o acesso ao pool de Session Beans. Após configurar o acesso, invocamos o método obterTodos, na
construção da resposta ao HTTP no modo GET, aceitando uma chamada como a apresentada a seguir.
http://localhost:8080/ExemploEJB-war/ServletListaProduto
Caso esteja tudo correto, teremos uma saída similar à que vemos a seguir, na tela do navegador.
Implementação de MVC no ambiente Java
A maioria dos sistemas possui diversas entidades de negócio que podem se relacionar. Esse
relacionamento deve ser implementado nas camadas de banco e modelos. Contudo, esse tipo de situação
costuma extrapolar as camadas mais baixas, reverberando até nas visualizações.
Roteiro de prática
Utilize o exemplo de sistema de produtos e adicione as camadas correspondentes à entidade
"empresa". Um produto deverá possuir a apenas uma empresa. A empresa, por sua vez, pode possuir
diversos produtos. Para executar os novos requisitos, siga as etapas:
• Criar as tabelas e inserir os dados de empresas e produtos.
• Crie a entidade "empresa" a partir do banco de dados.
• Desenvolva um EJB para intermediar a comunicação com o modelo Produto.
• Crie um Servlet responsável pela Listagem de produtos.
Veja o resultado dessa prática.
cria_registros.sql:
CREATE TABLE EMPRESA (
CODIGO INT NOT NULL PRIMARY KEY,
RAZAO_SOCIAL VARCHAR(50));
CREATE TABLE PRODUTO (
CODIGO INT NOT NULL PRIMARY KEY,
Padrões de Projetos de Software com Java
Marcio Quirino - 157
NOME VARCHAR(50),
QUANTIDADE INTEGER,
COD_EMPRESA INT NOT NULL);
ALTER TABLE PRODUTO ADD FOREIGN KEY(COD_EMPRESA)
REFERENCES EMPRESA(CODIGO);
INSERT INTO EMPRESA VALUES (1,'SKY NET');
INSERT INTO EMPRESA VALUES (2,'MATRIX');
INSERT INTO PRODUTO VALUES (1,'Morango',200, 1);
INSERT INTO PRODUTO VALUES (2,'Banana',1000, 1);
INSERT INTO PRODUTO VALUES (3,'Manga',600, 2);
Produto.java
@Entity
@Table(name = "PRODUTO")
@NamedQueries({
@NamedQuery(name = "Produto.findAll", query = "SELECT p FROM Produto p")})
public class Produto implements Serializable {
private static final long serialVersionUID = 1L;em Java é o Apache NetBeans, cujo instalador pode ser encontrado no site do
ambiente, acessando o menu Download. Porém, antes de instalar o NetBeans, é necessário ter instalado o
JDK (Java Development Kit) referente à edição Java SE (Standard Edition), que pode ser encontrado no site
da Oracle Technology Network: Java SE - Downloads | Oracle Technology Network | Oracle.
Os códigos com exemplos de aplicação dos padrões estão disponíveis ao longo do conteúdo em
formato texto, bastando copiar para inserir no ambiente de programação.
Introdução
Os padrões GoF, do inglês “Gang of Four”, são padrões de projeto orientado a objetos divididos em
três categorias: de criação, estruturais e comportamentais. São assim denominados por terem sido
introduzidos pelos quatro autores do livro Design Patterns: Elements of Reusable Object-Oriented Software
(GAMMA et al., 1994).
Padrões de Projetos de Software com Java
Marcio Quirino - 10
Os padrões de projeto GoF de criação nos ajudam a construir sistemas independentes da forma com
que os objetos são criados e representados. Quando criamos um objeto da classe B em um método da
classe A por meio de um simples comando “new B()”, estabelecemos uma relação de dependência entre
duas implementações, visto que, em Java, uma classe é uma implementação concreta de um conjunto de
operações. Neste exemplo, portanto, a classe A é dependente da classe B.
Em várias situações, entretanto, a criação de uma relação de dependência entre duas ou mais
implementações torna o sistema inflexível, dificultando a sua evolução. Os princípios SOLID de Inversão
de Dependências e Open Closed nos orientam a fazer as implementações dependerem de abstrações,
especialmente em casos nos quais um módulo depende de um serviço que possa ter diferentes
implementações.
Princípios SOLID
O acrônimo SOLID se refere, na programação orientada a objetos, a cinco princípios ou postulados de design,
destinados a facilitar a compreensão, o desenvolvimento e a manutenção de software. São eles:
• Single-responsibility (responsabilidade única);
• Open-closed (aberto-fechado);
• Liskov substitution (substituição de Liskov);
• Interface segregation (segregação de interface);
• Dependency inversion (inversão de dependência).
Imagine um módulo que utilize um intermediador de pagamentos fornecido por uma empresa externa.
Existe a chance de mudarmos a empresa fornecedora ou de termos de trabalhar com mais de uma empresa?
Claro que sim!
• Nesse caso, não é uma boa ideia fazer os módulos do nosso sistema dependerem da
implementação de uma empresa específica.
• Os módulos devem, portanto, depender de abstrações, de forma que seja possível trabalhar
com diferentes intermediadores de pagamentos, sem haver necessidade de alterar os
módulos clientes desse serviço.
Entretanto, para utilizarmos as implementações específicas, precisamos instanciar objetos referentes
a essas implementações. Como podemos instanciar esses objetos sem estabelecermos dependências
indesejáveis? Esse é um dos problemas fundamentais tratados pelos padrões de projeto GoF do grupo
Criação.
Padrões de Projetos de Software com Java
Marcio Quirino - 11
Neste conteúdo, você aprenderá os cinco padrões desse grupo: Factory Method, Abstract Factory,
Builder, Prototype e Singleton. Esses padrões encapsulam o conhecimento sobre as classes concretas que
o sistema utiliza e sobre como as instâncias dessas classes são criadas, permitindo que os demais módulos
do sistema trabalhem com interfaces abstratas em lugar de implementações específicas.
Projetos construídos com a aplicação correta desses padrões possuem maior flexibilidade em
relação aos objetos criados, aos criadores desses objetos e a como e quando esses objetos são criados,
facilitando a implementação de variações e evoluções do sistema.
1. Padrão de projeto Factory Method
Intenção do padrão Factory Method
Factory Method é um padrão frequentemente utilizado na implementação de frameworks.
Ele define uma interface para a criação de objetos, deixando para as subclasses a decisão sobre a
classe específica a ser instanciada.
Problema do padrão Factory Method
Você conhece o problema que o padrão Factory Method busca resolver?
Suponha que a sua tarefa seja implementar um método que remova os itens inválidos de uma
coleção de itens de um pedido. Um item de pedido possui os seguintes atributos:
1. A quantidade
2. O preço unitário
3. O produto solicitado
Imagine que um item válido é aquele que tenha uma quantidade de 1 a 100.
Veja a implementação da classe ItemPedido a seguir.
Além dos atributos, das operações de acesso e do construtor, essa classe define a operação valor,
que retorna o valor do item resultante da multiplicação da quantidade pelo preço unitário.
1 public class ItemPedido {
2 private Produto produto;
3 private int quantidade;
4 private int precoUnitarioEmCentavos;
5
6 public ItemPedido(Produto produto, int quantidade, int valorEmCentavos) {
7 this.produto = produto;
8 this.quantidade = quantidade;
9 this.precoUnitarioEmCentavos = valorEmCentavos;
10 }
11 public int valor() {
12 return quantidade * precoUnitarioEmCentavos;
13 }
14 public Produto getProduto() {
15 return produto;
16 }
17 public void setProduto(Produto produto) {
18 this.produto = produto;
19 }
20 public int getQuantidade() {
21 return quantidade;
22 }
23 public void setQuantidade(int quantidade) {
24 this.quantidade = quantidade;
25 }
26 public int getPrecoUnitarioEmCentavos() {
27 return precoUnitarioEmCentavos;
Padrões de Projetos de Software com Java
Marcio Quirino - 12
28 }
29 public void setPrecoUnitarioEmCentavos(int precoUnitarioEmCentavos) {
30 this.precoUnitarioEmCentavos = precoUnitarioEmCentavos;
31 }
32 }
Uma primeira implementação da operação de remoção dos itens inválidos está listada a seguir:
1 public void removerItensInvalidos(ArrayList‹ItemPedido› itens) {
2 ArrayIterator‹ItemPedido› cursor = new ArrayIterator(itens);
3 while (cursor.hasNext()) {
4 ItemPedido item = cursor.next();
5 if (! isValido(item)) {
6 cursor.remove(item);
7 }
8 }
9 }
10
11 public boolean isValido(ItemPedido item) {
12 return (item.getQuantidade() › 0 && item.getQuantidade() ‹ 100);
13 }
Nessa implementação, imagine que definimos uma classe ArrayIterator que implementa um cursor
sobre os itens de pedido recebidos como parâmetro com as seguintes operações:
A. hasNext
✓ Que verifica se existe um próximo elemento no ArrayList ou se o cursor já está posicionado
no último elemento.
B. next
✓ Que retorna o próximo elemento do ArrayList. Na primeira chamada, ele retorna o primeiro
elemento da coleção.
C. remove
✓ Que remove um elemento da coleção.
Você consegue identificar o principal problema dessa solução?
Embora funcione, essa solução utiliza a classe ArrayList, criando um acoplamento da implementação
com uma forma específica de organização dos itens.
Suponha que os pedidos passem a ser organizados em um HashSet, por exemplo. O efeito negativo
desse acoplamento fica evidente, pois teremos de modificar a implementação, uma vez que a forma de
percurso em um HashSet é diferente daquela utilizada em um ArrayList.
Poderíamos desenvolver outra versão específica para um HashSet, definindo uma classe
HashSetIterator e implementando as mesmas operações da classe ArrayListIterator, mas com um algoritmo
específico para o percurso e a manipulação dos elementos.
O código a seguir apresenta a versão da operação removerItensInvalidos implementada a partir de
um HashSet:
1 public void removerItensInvalidos(HashSet‹ ItemPedido› itens) {
2 HashSetIterator‹ItemPedido› cursor = new HashSetIterator(itens);
3 while (cursor.hasNext()) {
4 ItemPedido item = cursor.next();
5 if (! isValido(item)) {
6 cursor.remove(item);
7 }
8 }
9 }
Imagine, agora,que houvesse diversos outros tipos de coleção. Você faria uma nova implementação
para cada tipo específico de coleção?
Padrões de Projetos de Software com Java
Marcio Quirino - 13
• Indo além, imagine que esse problema que você enfrentou em uma operação específica do
sistema (isto é, remover itens de pedido inválidos) ocorra em dezenas de outras situações do
mesmo sistema. O resultado será uma enorme replicação de código, que é um dos principais
inimigos da evolução sustentável de um sistema.
• Note que as duas implementações apresentadas são muito parecidas, diferindo apenas pelo
tipo de coleção e do cursor criado. Considerando que todas as coleções implementam um
tipo específico Collection, uma alternativa seria definir uma única operação
removerltenslnvalidos e instanciar o cursor específico para a coleção recebida como
parâmetro.
O código a seguir apresenta essa implementação alternativa:
1 public void removerItensInvalidos(Collection‹ItemPedido› itens) throws Exception {
2 Iterator‹ItemPedido› cursor = null;
3 if (itens instanceof ArrayList)
4 cursor = new ArrayIterator((ArrayList) itens);
5 else if (itens instanceof HashSet)
6 cursor = new HashSetIterator((HashSet) itens);
7
8 if (cursor == null)
9 throw new Exception("tipo da coleção de itens inválido");
10
11 while (cursor.hasNext()) {
12 ItemPedido item = cursor.next();
13 if (! isValido(item)) {
14 cursor.remove(item);
15 }
16 }
17 }
Com essa solução, implementamos apenas uma operação removerItensInvalidos capaz de operar
com um ArrayList ou com um HashSet. Agora, imagine que existissem vários outros tipos de coleção.
Você consegue visualizar a enorme quantidade de comandos condicionais que deveriam ser
adicionados?
Portanto, esse código teria de ser modificado a cada novo tipo de implementação de coleção,
acumulando uma quantidade significativa de expressões condicionais, o que é uma violação clara do
princípio Open Closed – um dos princípios SOLID.
Além disso, esse código apresenta estruturas baseadas em downcasting, o que é um indicativo de
deficiência na estrutura da solução. O downcasting está presente na conversão da coleção de itens para
ArrayList ou para HashSet, dependendo do tipo da coleção recebida como parâmetro.
Comentário
Perceba como essa implementação adiciona complexidade em relação à implementação anterior. Você deve
ter sempre em mente que alta complexidade também é um dos principais inimigos da evolução sustentável de um
sistema.
O problema específico, portanto, consiste em implementar o método removerItensInvalidos, e todos
os demais métodos nos quais você precise percorrer e interagir com uma coleção de objetos, de modo que
ele funcione com qualquer forma de organização dessa coleção, sem que haja necessidade de recorrer a
soluções baseadas em clonagem ou em estruturas condicionais complexas presentes nos exemplos
apresentados.
Atenção
O problema mais geral resolvido pelo Factory Method é fazer com que um módulo cliente não precise instanciar
diretamente uma dentre várias possíveis implementações de uma abstração, tornando-o, portanto, dependente apenas
da abstração e não de suas implementações específicas.
Padrões de Projetos de Software com Java
Marcio Quirino - 14
Solução do padrão Factory Method
O framework de estrutura de dados da linguagem Java implementa uma solução para esse problema
por meio da aplicação do padrão Factory Method.
As estruturas de dados são classes que implementam uma interface genérica chamada Collection.
São exemplos de diferentes implementações dessa interface:
A. ArrayList
✓ Que representa estruturas como arrays.
B. LinkedList
✓ Que representa estruturas como listas encadeadas.
C. HashSet
✓ Que representa estruturas como conjuntos chave-valor.
D. TreeSet
✓ Que representa estruturas como conjuntos organizados em árvores de busca.
A organização dessas classes está ilustrada, de forma simplificada, no diagrama de classes a seguir:
A interface Collection define uma operação abstrata chamada iterator, que é implementada em cada
estrutura de dados específica. Essa operação cria e retorna um objeto que implementa a interface Iterator.
Atenção
A interface Iterator define um cursor que possibilita a navegação em uma coleção de dados e a exclusão de
elementos com as mesmas operações apresentadas nos exemplos anteriores, isto é, hasNext, next e remove.
Você deve ter percebido que a implementação do percurso depende da forma com que os dados são
organizados na coleção. A remoção de um elemento da coleção também é dependente da forma como seus
elementos são estruturados. Isso significa que existe uma implementação da interface Iterator para cada
classe que implementa a interface Collection, como você pode ver na imagem a seguir:
Padrões de Projetos de Software com Java
Marcio Quirino - 15
Dessa forma, a operação iterator de ArrayList instancia um ArrayIterator, a de LinkedList instancia
um ListIterator, e assim por diante, para cada coleção específica. Esse esquema é uma simplificação, para
fins didáticos, das classes realmente implementadas na linguagem Java.
Veja, no código a seguir, como você poderia implementar a operação removerItensInvalidos usando
esse framework:
1 public void removerItensInvalidos(Collection‹ItemPedido› itens) {
2 Iterator‹ItemPedido› iterator = itens.iterator();
3 while (iterator.hasNext()) {
4 ItemPedido item = iterator.next();
5 if (! isValido(item)) {
6 itens.remove(item);
7 }
8 }
9 }
O comando itens.iterator() é uma chamada polimórfica a partir da interface Collection, que resulta na
criação de uma das implementações específicas da interface Iterator.
Portanto, se a Collection for um ArrayList, por exemplo, esse comando será executado pela classe
ArrayList, que criará um ArrayIterator e retornará essa instância (cursor), a qual será referenciada pela
variável iterator.
Comentário
Note que, nessa solução, a única responsabilidade da operação iterator é criar (fabricar) a instância da interface
Iterator apropriada para a estrutura de dados. Esta é a ideia central do padrão Factory Method: definir uma operação
“fábrica” na classe abstrata (Collection), deixando para cada subclasse (ArrayList, LinkedList, HashSet, TreeSet) a
decisão da implementação específica da interface (Iterator) retornada pela fábrica.
Na imagem a seguir, você pode observar que a estrutura geral da solução proposta pelo padrão
Factory Method define quatro participantes:
Padrões de Projetos de Software com Java
Marcio Quirino - 16
Do lado esquerdo, estão os produtos a serem fabricados. O participante Product corresponde ao tipo
genérico do elemento a ser fabricado, enquanto ConcreteProduct corresponde a cada especialização do
produto a ser fabricado.
No exemplo das estruturas de dados, a interface Iterator desempenha o papel de Product, enquanto
as classes ArrayIterator, ListIterator, KeyIterator e ValueIterator desempenham o papel de ConcreteProduct.
Do lado direito, estão os criadores, isto é, as classes que são responsáveis pela instanciação dos
produtos. O participante Creator define uma operação (factoryMethod) que retorna uma instância da
interface genérica Product, enquanto ConcreteCreator representa cada implementação concreta de Creator
responsável pela instanciação do ConcreteProduct específico.
Portanto, no exemplo das estruturas de dados, a interface Collection corresponde ao participante
Creator, e sua operação abstrata iterator é o factoryMethod. Já as classes ArrayList, LinkedList, HashSet e
TreeSet desempenham o papel de ConcreteCreator e são responsáveis pela implementação da operação
factoryMethod, na qual será feita a instanciação de um ArrayIterator, ListIterator, KeyIterator e ValueIterator
(ConcreteProduct), respectivamente.
Consequências e padrões relacionados ao Factory Method
O padrão FactoryMethod permite que diferentes implementações de um mesmo serviço possam ser
utilizadas por um cliente sem que seja necessário replicar códigos similares ou utilizar estruturas
condicionais complexas, conforme ilustrado no exemplo anterior.
Além disso, esse padrão possibilita a conexão de duas hierarquias paralelas representadas pelos
participantes genéricos Creator e Product.
O Factory Method é muito útil quando precisamos segregar uma hierarquia de objetos
detentores de informações (objetos de domínio) dos diferentes algoritmos de manipulação dessas
informações.
Portanto, esse padrão pode ser aplicado em conjunto com o padrão Strategy, que tem como objetivo
a separação de diferentes algoritmos dos objetos de domínio sobre os quais eles atuam.
Template Method é outro padrão frequentemente utilizado em conjunto com o Factory Method.
O Template Method é uma implementação genérica definida em uma superclasse que contém
passos que podem ser especializados nas subclasses.
Padrões de Projetos de Software com Java
Marcio Quirino - 17
Um desses passos pode corresponder à criação de um objeto específico, que pode ser realizada
pela aplicação do padrão Factory Method.
2. Padrão de projeto Abstract Factory
Intenção do padrão Abstract Factory
Abstract Factory é um padrão que fornece uma interface para a criação de famílias de objetos
relacionados ou dependentes, sem criar dependências entre o cliente e as classes concretas instanciadas.
Problema do padrão Abstract Factory
Imagine que você esteja trabalhando em uma implementação que tenha integrações com sistemas
externos de diferentes organizações. Considere que os sistemas de cada organização externa enviem os
mesmos tipos de mensagens em diferentes formatos, como, por exemplo, texto contendo campos de
tamanho predefinido, XML, CSV, entre outros. Considere, ainda, que cada organização envie suas
mensagens sempre no mesmo formato.
O quadro a seguir apresenta um exemplo de integração com três organizações que enviam
mensagens codificadas no formato especificado:
Emissor Mensagem Formato
Organização X Registrar Cliente XML
Organização Y Registrar Cliente CSV
Organização Z Registrar Cliente Campos de tamanho fixo
Organização X Registrar Conta XML
Organização Y Registrar Conta CSV
Organização Z Registrar Conta Campos de tamanho fixo
Quadro elaborado por Alexandre Correa
Podemos definir um conjunto de classes responsáveis pela decodificação de mensagens de um
formato específico em objetos independentes desse formato.
A imagem a seguir apresenta a estrutura dessa solução:
A interface RegistrarClienteDecoder representa um serviço genérico que traduz uma mensagem de
registro de cliente, recebida em um formato qualquer, para um objeto da classe MsgRegistrarCliente, que
corresponde à representação independente de formato da mensagem recebida.
Padrões de Projetos de Software com Java
Marcio Quirino - 18
As implementações dessa interface para cada formato específico são:
A. XML
✓ classe RegistrarClienteXMLDecoder
B. Texto Fixo
✓ classe RegistrarClienteTextoFixoDecoder
C. CSV
✓ classe RegistrarClienteCSVDecoder
Atenção
Para cada mensagem recebida pelo sistema (como, por exemplo, Registrar Cliente, Registrar Conta), deve ser
criada uma estrutura de classes similar à apresentada.
O código a seguir corresponde ao esqueleto de implementação da classe ServicoIntegracao: um
exemplo de módulo que utiliza as classes de decodificação.
As operações dessa classe representam as mensagens recebidas das diferentes organizações.
A operação registrarCliente, por exemplo, recebe um texto com o conteúdo da mensagem enviada
por uma origem. Essa origem é codificada em um texto (X, Y ou Z), representando as diferentes
organizações. O texto da mensagem deve ser decodificado do formato específico em uma instância da
classe MsgRegistrarCliente para seu posterior tratamento.
1 public class ServicoIntegracao {
2 public void registrarCliente (String textoMsg, String origem) {
3 RegistrarClienteDecoder msgDecoder = null;
4
5 if (“X”.equals(origem)) {
6 msgDecoder = new RegistrarClienteXMLDecoder();
7 } else if ("Y".equals(origem)) {
8 msgDecoder = new RegistrarClienteCSVDecoder();
9 } else if ("Z".equals(origem)) {
10 msgDecoder = new RegistrarClienteTextoFixoDecoder();
11 }
12 MsgRegistrarCliente msg = msgDecoder.decode(textoMsg);
13 ...
14 // código para o tratamento da mensagem recebida
15 }
16
17 public void registrarConta (String textoMsg, String origem) {
18 RegistrarContaDecoder msgDecoder = null;
19
20 if (“X”.equals(origem)) {
21 msgDecoder = new RegistrarContaXMLDecoder();
22 } else if ("Y".equals(origem)) {
23 msgDecoder = new RegistrarContaCSVDecoder();
24 } else if ("Z".equals(origem)) {
25 msgDecoder = new RegistrarContaTextoFixoDecoder();
26 }
27 MsgRegistrarConta msg = msgDecoder.decode(textoMsg);
28 ...
29 // código para o tratamento da mensagem recebida
30 }
31 ... // operações para recepção e tratamento das demais mensagens
32 }
Você consegue identificar o problema dessa implementação da classe Servicolntegracao?
Ela está acoplada com todos os tipos possíveis de decodificadores, concentrando toda a
complexidade de resolução sobre o decodificador apropriado para traduzir uma mensagem vinda de
determinada origem.
Padrões de Projetos de Software com Java
Marcio Quirino - 19
Comentário
Você deve ter percebido que, caso novos formatos e origens sejam adicionados, esse código terá de ser
modificado, o que configura uma violação do princípio Open Closed, um dos princípios SOLID.
Nesse exemplo, temos várias famílias de decodificadores de acordo com o formato da mensagem,
como: decodificadores XML, CSV e Texto Fixo.
1. Ao recebermos uma mensagem da origem X, por exemplo, sabemos que precisamos utilizar o
conversor XML correspondente a essa mensagem, pois essa origem envia todas as suas
mensagens no formato XML.
2. Portanto, o problema tratado pelo padrão Abstract Factory consiste em isolar o cliente de uma
família de produtos relacionados de suas implementações específicas, respondendo à seguinte
pergunta:
a. Como podemos remover todas as instanciações dos decodificadores da classe
Servicolntegracao, criando uma solução genérica que permita que esse serviço trabalhe
com novos formatos de decodificação sem que seu código precise ser alterado?
Solução do padrão Abstract Factory
A estrutura da solução proposta pelo padrão Abstract Factory está representada no diagrama de
classes a seguir:
Do lado direito, estão os vários produtos criados pelas fábricas. Cada tipo de produto é definido por
uma interface genérica (AbstractProduct_A e AbstractProduct_B) e possui diversas implementações que
definem os objetos específicos a serem criados pelas fábricas. Product_A1 e Product_A2, por exemplo, são
implementações concretas de AbstractProduct_A.
No problema apresentado anteriormente, a interface RegistrarClienteDecoder corresponde ao
participante AbstractProduct_A, enquanto as classes RegistrarClienteXMLDecoder,
RegistrarClienteTextoFixoDecoder e RegistrarClienteCSVDecoder representam os produtos concretos
Product_A1, Product_A2 e Product_A3.
Padrões de Projetos de Software com Java
Marcio Quirino - 20
De forma análoga, a interface RegistrarContaDecoder corresponde ao participante
AbstractProduct_B, enquanto as classes RegistrarContaXMLDecoder, RegistrarContaTextoFixoDecoder e
RegistrarContaCSVDecoder representam os produtos concretos Product_B1, Product_B2 e Product_B3.
Do lado esquerdo, estão as fábricas. Cada fábrica é responsável por criar instâncias específicas de
uma família definida por seus produtos abstratos.
Dessa forma, a fábrica ConcreteFactory_1 é responsável por criar instâncias das classes Product_A1
e Product_B1, enquanto a fábrica ConcreteFactory_2 é responsávelpor criar instâncias das classes
Product_A2 e Product_B2. Portanto, Product_A1 e Product_B1 formam uma família de produtos, enquanto
Product_A2 e Product_B2 formam outra família de produtos.
Vamos ver como ficaria a nova solução para o serviço de integração com a utilização desse
padrão?
Primeiro, precisamos definir as fábricas. Podemos definir as famílias de decodificadores de acordo
com o formato das mensagens, conforme ilustrado na imagem a seguir:
A classe DecoderFactory representa o participante AbstractFactory do padrão, definindo uma
interface genérica para a criação dos diversos decodificadores de mensagens. Cada especialização dessa
classe corresponde ao participante ConcreteFactory do padrão, sendo responsável pela criação dos
decodificadores correspondentes a um formato específico de mensagem (XML, CSV ou TextoFixo).
A implementação dessa estrutura é apresentada a seguir. Note que a responsabilidade de cada
fábrica é apenas instanciar a classe de um decodificador específico da família (XML, CSV etc.). A fábrica
abstrata possui uma operação adicional (fabricaParaOrigem) que retorna a fábrica apropriada para
determinada origem. Ela funciona como uma espécie de registro de todas as fábricas e suas respectivas
origens.
1 public abstract class DecoderFactory {
2 public abstract RegistrarClienteDecoder createRegistrarClienteDecoder();
3 public abstract RegistrarContaDecoder createRegistrarContaDecoder();
4
5 public static DecoderFactory fabricaParaOrigem(String origem) {
6 if (“X”.equals(origem)) {
7 return new XMLDecoderFactory();
8 } else if ("Y".equals(origem)) {
9 return new CSVDecoderFactory();
10 } else if ("Z".equals(origem)) {
11 return new TextoFixoDecoderFactory();
Padrões de Projetos de Software com Java
Marcio Quirino - 21
12 }
13 }
14 }
15
16 public class XMLDecoderFactory extends DecoderFactory {
17 public RegistrarClienteDecoder createRegistrarClienteDecoder() {
18 return new RegistrarClienteXMLDecoder();
19 }
20 public RegistrarContaDecoder createRegistrarContaDecoder() {
21 return new RegistrarContaXMLDecoder();
22 }
23 }
24
25 public class CSVDecoderFactory extends DecoderFactory {
26 public RegistrarClienteDecoder createRegistrarClienteDecoder() {
27 return new RegistrarClienteCSVDecoder();
28 }
29 public RegistrarContaDecoder createRegistrarContaDecoder() {
30 return new RegistrarContaCSVDecoder();
31 }
32 }
Agora, vamos utilizar as fábricas para modificar a implementação das operações da classe
ServicoIntegracao.
Veja, no código a seguir, que a operação registrarCliente chama a operação fabricaParaOrigem, a
partir da origem recebida como parâmetro, para obter a instância da fábrica apropriada para as mensagens
recebidas dessa origem.
Na sequência, a chamada para a operação createRegistrarClienteDecoder da fábrica cria o
decodificador específico para a mensagem RegistrarCliente.
1 public class ServicoIntegracao {
2 public void registrarCliente (String textoMsg, String origem) {
3 DecoderFactory decoderFactory = DecoderFactory.fabricaParaOrigem(origem);
4 RegistrarClienteDecodermsgDecoder =
decoderFactory.createRegistrarClienteDecoder();
5 MsgRegistrarCliente msg = msgDecoder.decode(textoMsg);
6 ...
7 // código para tratamento da mensagem MsgRegistrarCliente
8 }
9
10 public void registrarConta (String textoMsg, String origem) {
11 DecoderFactory decoderFactory = DecoderFactory.fabricaParaOrigem(origem);
12 RegistrarContaDecoder msgDecoder =
decoderFactory.createRegistrarContaDecoder();
13 MsgRegistrarConta msg = msgDecoder.decode(textoMsg);
14 ...
15 // código para tratamento da mensagem MsgRegistrarConta
16 }
17
18 ... // código para demais mensagens
19 }
Note que esse código não precisará ser modificado para novos formatos de mensagem, pois bastará
adicionar novos decodificadores e definir uma nova fábrica concreta. Além disso, a estrutura do código ficou
muito mais enxuta e menos complexa.
Esse padrão é utilizado, por exemplo, na implementação do framework AWT de interface com o
usuário da linguagem Java. Os componentes visuais específicos de plataforma, como Windows e Motif, por
exemplo, formam uma família de produtos (Button, Frame, Panel etc.). A classe Toolkit corresponde à fábrica
abstrata que oferece operações de criação de cada componente visual. Cada plataforma é implementada
em uma subclasse de Toolkit específica.
Padrões de Projetos de Software com Java
Marcio Quirino - 22
AWT
Abstract Window Toolkit é o toolkit gráfico original da linguagem de programação Java.
Motif
Interface gráfica padrão para usuários de sistema operacional Unix.
Consequências e padrões relacionados ao Abstract Factory
O padrão Abstract Factory promove o encapsulamento do processo de criação de objetos, isolando
os clientes das implementações concretas, permitindo que os clientes sejam implementados pelo uso
apenas de abstrações.
Além disso, esse padrão promove a consistência entre produtos relacionados, isto é, produtos da
mesma família que devem ser utilizados em conjunto. Entretanto, a introdução de novos produtos não é
simples, pois exige mudança em todas as fábricas.
Atenção
Cada novo produto inserido exige a adição de uma nova operação de criação em cada fábrica da estrutura.
O padrão Abstract Factory está relacionado com outros padrões de criação. Cada operação de
criação é tipicamente implementada utilizando o padrão Factory Method.
É possível configurar fábricas mais flexíveis utilizando o padrão Prototype. Cada fábrica concreta
pode ser definida como um Singleton, já que, normalmente, apenas uma instância de uma fábrica específica
precisa ser instanciada.
No exemplo apresentado neste módulo, poderíamos ainda eliminar a duplicação de código similar
presente nas operações registrarCliente e registrarConta, generalizando a fábrica para retornar objetos de
um tipo genérico Decoder (ao invés de decodificadores específicos) e transformando essas operações em
objetos por meio da aplicação de outros padrões, como o Command e o Template Method, por exemplo.
Desafio
Estude os padrões mencionados e tente modificar a estrutura do exemplo, aplicando-os na nova solução.
3. Padrão de projeto Builder
Intenção do padrão Builder
Builder é um padrão que visa separar a construção de um objeto complexo de sua representação,
de forma que o mesmo processo de construção possa construir diferentes representações desse objeto.
Problema do padrão Builder
Suponha que você esteja fazendo um sistema para uma corretora de valores mobiliários, e que esse
sistema permita que o cliente exporte suas notas de negociação em diferentes formatos, tais como: XML,
PDF ou XLS.
Imagine que o processo de construção de qualquer representação da nota de negociação seja
definido por três passos fundamentais:
• Construir o cabeçalho da nota.
• Listar as operações da nota.
• Gerar o sumário com os totais e taxas de todas as operações do dia.
Uma solução frequente utilizada para tal problema é definir todas as possíveis conversões em uma
única classe, como ilustrado no código a seguir:
Padrões de Projetos de Software com Java
Marcio Quirino - 23
1 public class ExportadorNota {
2 public byte[] exportarNota(NotaNegociacao nota, String formato) {
3 if (“XML”.equals(formato))
4 return gerarNotaXML(nota);
5 else if (“PDF”.equals(formato))
6 return gerarNotaPDF(nota);
7 else if (“XLS”.equals(formato))
8 return gerarNotaXLS(nota);
9 }
10
11 private byte[] gerarNotaXML(NotaNegociacao nota) {
12 // construir cabeçalho em XML
13 // listar os itens da nota em XML
14 // gerar sumário em XML
15 // retornar conteúdo da nota no formato XML
16 }
17
18 private byte[] gerarNotaPDF(NotaNegociacao nota) {
19 // construir cabeçalho em PDF
20 // listar os itens da nota em PDF
21 // gerar sumário