Prévia do material em texto
Padrões GoF estruturais
Os padrões estruturais GoF organizam classes e objetos, possibilitando a formação de estruturas
complexas. Eles incluem Adapter, que compatibiliza interfaces; Bridge, que separa abstração e
implementação; Composite, que uniformiza tratamento de objetos e composições; Decorator, que adiciona
funcionalidades dinamicamente; Facade, que simplifica interações; Flyweight, que otimiza memória
compartilhando dados; e Proxy, que controla acesso a objetos.
Alexandre Luís Correa e Prof. Marcelo Perantoni
1. Itens iniciais
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 da licença em seu e-mail
institucional. Ao recebê-la, 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.
Objetivos
Reconhecer o propósito, a estrutura e as situações de aplicação do padrão de projeto Adapter.
Reconhecer o propósito, a estrutura e as situações de aplicação dos padrões de projeto Bridge e
Decorator.
Reconhecer o propósito, a estrutura e as situações de aplicação dos padrões de projeto Composite e
Facade.
Reconhecer o propósito, a estrutura e as situações de aplicação dos padrões de projeto Flyweight e
Proxy.
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.
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
•
•
•
•
https://stecine.azureedge.net/repositorio/00212ti/02685/docs/astah.zip
https://stecine.azureedge.net/repositorio/00212ti/02685/docs/astah.zip
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.
No vídeo a seguir, você acompanhará a apresentação do tema Padrões GoF estruturais, que inclui os padrões
de projeto Adapter, Bridge, Decorator, Composite, Facade, Flyweight e Proxy.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
Padrão de tomada diferente do brasileiro.
1. Padrão de projeto Adapter
Intenção e problema resolvido pelo padrão Adapter
O padrão Adapter do GoF converte a interface de uma classe em outra interface que os clientes esperam,
permitindo que classes com interfaces incompatíveis trabalhem juntas. Ele atua como um tradutor, adaptando
a interface de uma classe existente à interface necessária, sem alterar seu código interno.
O propósito do Adapter é facilitar a integração e reutilização de classes existentes em novos contextos,
especialmente em sistemas legados, proporcionando flexibilidade e compatibilidade sem a necessidade de
modificar as classes originais. É útil quando se deseja que uma classe funcione com outra que possua uma
interface diferente.
Você vai compreender neste vídeo a intenção e o problema resolvido pelo padrão Adapter, permitindo que
classes com interfaces incompatíveis trabalhem juntas, adaptando uma interface existente para a interface
esperada pelo cliente, promovendo reutilização de código e flexibilidade.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
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? Veja a situação hipotética a seguir:
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. Vejamos a seguir:
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 relizaçã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íveispublic 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. Veja o que as classes a seguir definem:
Classe SubstanciaSimples
A classe SubstanciaSimples tem o nome
herdado da superclasse e define o número de
átomos do respectivo elemento químico.
Classe SubstanciaComposta
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.
java
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.
Atividade 1
Assinale a alternativa que expressa a intenção do padrão de projeto Flyweight:
A
Permitir a utilização de objetos remotos de forma transparente para o módulo chamador, isto é, o módulo
chamador pode chamar as operações do objeto remoto como se ambos estivessem rodando no mesmo
processo.
B
Prevenir a instanciação desnecessária de objetos com uma carga muito custosa, como objetos contendo
imagens, por exemplo, de modo que essa carga possa ser feita de forma transparente para o módulo cliente,
quando necessário.
C
Permitir a utilização mais eficiente de memória, evitando a criação de um número muito grande de objetos
imutáveis simples, por meio de uma solução baseada em compartilhamento.
D
Permitir a utilização de diferentes implementações de um mesmo serviço oferecidas por terceiros, a partir de
uma interface comum.
E
Oferecer uma interface simplificada de alto nível de abstração para um cliente acessar os serviços oferecidos
por um subsistema.
A alternativa C está correta.
A alternativa A corresponde a um remote proxy, enquanto a alternativa B corresponde a um virtual proxy. As
alternativas D e E correspondem, respectivamente, às intenções dos padrões Adapter e Facade.
Solução, consequências e padrões relacionados ao padrão
Flyweight
O padrão Flyweight soluciona o problema do alto consumo de memória ao compartilhar dados intrínsecos
entre muitos objetos semelhantes. A abordagem separa os dados compartilháveis (intrínsecos) dos dados
específicos de cada objeto (extrínsecos), armazenando apenas uma instância dos dados intrínsecos. As
consequências incluem a significativa redução do uso de memória e a potencial complexidade adicional na
gerência dos estados extrínsecos.
Padrões relacionados incluem Factory, que pode ser usado para gerenciar a criação de instâncias; e Singleton,
que garante uma única instância compartilhada dos dados intrínsecos. Ambos promovem a eficiência na
utilização de recursos e a modularidade.
No vídeo, você vai entender a solução para o problema resolvido pelo padrão Flyweight, que permite
minimizar o uso de memória compartilhando o máximo de dados possível entre objetos semelhantes,
promovendo eficiência e reduzindo a sobrecarga de criação de muitos objetos semelhantes, bem como as
consequências e padrões relacionados.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
Solução do padrão Flyweight
A estrutura da solução proposta pelo padrão Flyweight está representada no diagrama de classes a seguir.
Estrutura do padrão Flyweight.
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
Representa os objetos que são compartilhados.
UnsharedConcreteFlyweight
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.
O exemplo adiante mostra como o código das substâncias químicas pode ser escrito com a utilização do
padrão Flyweight.
Observe que 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.
Por fim, 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.
java
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 {
privateString 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 padrão 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.
Exemplo
Um tipo 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.
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. Veja a
comparação com os padrões State e Flyweight:
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.
No padrão Flyweight, 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?
Atividade 2
Imagine que você esteja estudando sobre o padrão Flyweight e analisa diferentes sites com conteúdos sobre
o assunto. Qual é a principal solução oferecida pelo padrão Flyweight nos padrões estruturais GoF? Marque a
opção correta.
A
Adicionar funcionalidades a objetos dinamicamente sem modificar sua estrutura original.
B
Converter a interface de uma classe em outra interface que o cliente espera.
C
Reduzir o consumo de memória compartilhando dados intrínsecos entre múltiplos objetos semelhantes.
D
Fornecer uma interface simplificada e unificada para um conjunto de interfaces em um subsistema complexo.
E
•
•
Permitir que objetos sejam compostos em estruturas de árvore para representar hierarquias parte-todo.
A alternativa C está correta.
O padrão Flyweight reduz o consumo de memória compartilhando dados intrínsecos entre muitos objetos
semelhantes. Ele separa os dados intrínsecos (compartilháveis) dos extrínsecos (específicos de cada
objeto), armazenando apenas uma instância dos dados intrínsecos. Isso permite uma representação
eficiente de um grande número de objetos, minimizando o uso de memória, essencial para sistemas que
requerem muitos objetos de granularidade fina.
Intenção e problema resolvido pelo padrão Proxy
O padrão Proxy tem como objetivo fornecer um substituto ou marcador para controlar o acesso a um objeto.
Ele resolve problemas relacionados ao controle de acesso, à otimização de recursos e ao encapsulamento de
complexidade, ao interpor um objeto substituto que gerencia a interação com o objeto real.
É interessante quando for necessário carregar objetos sob demanda (lazy loading), controlar o acesso a
recursos remotos, ou realizar operações adicionais antes de acessar o objeto real. O Proxy permite adicionar
uma camada de controle e proteção, melhorando a eficiência e a segurança do sistema sem alterar a interface
do objeto real.
Neste vídeo, você entenderá a intenção e o problema resolvido pelo padrão Proxy, que permite 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.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
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 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.
Imagem representativa do padrão Proxy.
Imagine que esses objetos estejam em
processos diversos, rodando em máquinas
diferentes, como 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.
Para você entender melhor o problema, veja o
código adiante, 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 conhece 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.
java
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);
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 o 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.
Atividade 3
Assinale a alternativa que expressa a intenção do padrão de projeto Proxy:
A
Permitir a instanciação de objetosa partir da geração de uma cópia de um objeto, fazendo com que o módulo
cliente não precise conhecer a classe específica que está sendo instanciada.
B
Permitir a utilização de objetos remotos, de forma transparente, para o módulo chamador, isto é, o módulo
chamador pode chamar as operações do objeto remoto como se ambos estivessem rodando no mesmo
processo.
C
Representar hierarquias de composição de objetos, de modo que, tanto os objetos individuais como os
agregados, possam ser gerenciados de maneira uniforme pelo mesmo conjunto de operações.
D
Permitir a adição de funcionalidades a objetos existentes sem utilizar herança.
E
Permitir a utilização mais eficiente de memória, por meio de uma solução baseada no compartilhamento de
objetos simples imutáveis.
A alternativa B está correta.
As alternativas A, C, D e E correspondem, respectivamente, às intenções dos padrões Clone, Composite,
Decorator, Flyweight.
Solução, consequências e padrões relacionados ao padrão
Proxy
O padrão Proxy soluciona o problema de controle de acesso e otimização de recursos ao interpor um objeto
substituto entre o cliente e o objeto real. A solução envolve criar um proxy que controla o acesso ao objeto
real, permitindo operações como carregamento sob demanda, controle de acesso, ou execução de tarefas
adicionais antes de delegar ao objeto real. As consequências incluem maior controle e eficiência, mas podem
introduzir complexidade adicional e potencial latência.
Padrões relacionados incluem Decorator, que adiciona responsabilidades a objetos dinamicamente; e Adapter,
que adapta interfaces para compatibilidade. Ambos promovem a flexibilidade e a modularidade no design de
sistemas.
Neste vídeo, você vai entender a solução para o problema resolvido pelo padrão Proxy, que permite 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, bem como as consequências e padrões relacionados.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
Solução do padrão Proxy
A estrutura da solução proposta pelo padrão Proxy está representada no diagrama de classes a seguir.
Estrutura do padrão Proxy.
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.
Dinâmica entre os participantes.
O principal participante do padrão é o elemento Proxy que controla o acesso ao objeto destino. Existem
diferentes tipos de proxy, como os apresentados a seguir:
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.
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.
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 adiante 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.
java
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);
}
return session;
}
public EJBHome getHome(String name, Class clazz) throws NamingException {
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:
java
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 padrão 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.
O padrão Proxy possui algumas similaridades com o padrão Adapter. Ambos oferecem uma forma de acesso
indireta ao objeto destino, porém, há distinção:
Já o padrão Decorator possui uma implementação similar a um proxy, mas tem um propósito diferente:
Atividade 4
Imagine que você seja convidado para uma palestra sobre padrões utilizados no desenvolvimento de
software. No final, você perguntou ao público qual é a principal solução oferecida pelo padrão Proxy nos
padrões estruturais GoF e obteve as respostas mostradas nas opções a seguir. Qual delas é a resposta
correta?
A
Adicionar funcionalidades a objetos dinamicamente sem modificar sua estrutura original.
Adapter
Traduz uma interface padrão em uma
interface específica oferecida, tipicamente,
por um componente de terceiros.
Proxy
Preserva a mesma interface oferecida
pelo objeto alvo.
Decorator
Adiciona novas funcionalidades a um
objeto.Proxy
Mantém as funcionalidades
oferecidas pelo objeto destino.
B
Converter a interface de uma classe em outra interface que o cliente espera.
C
Controlar o acesso a um objeto, permitindo operações como carregamento sob demanda e controle de
acesso.
D
Fornecer uma interface simplificada e unificada para um conjunto de interfaces em um subsistema complexo.
E
Permitir que objetos sejam compostos em estruturas de árvore para representar hierarquias parte-todo.
A alternativa C está correta.
O padrão Proxy controla o acesso a um objeto, permitindo operações como carregamento sob demanda,
controle de acesso, e execução de tarefas adicionais antes de delegar ao objeto real. Ele atua como um
intermediário que adiciona uma camada de controle, melhorando a eficiência, segurança e encapsulamento
da complexidade do objeto real, sem alterar sua interface.
5. Conclusão
Considerações finais
O que você aprendeu neste conteúdo?
Aplicação do padrão GoF de projeto estrutural Adapter.
Aplicação do padrão GoF de projeto estrutural Bridge.
Aplicação do padrão GoF de projeto estrutural Decorator.
Aplicação do padrão GoF de projeto estrutural Composite.
Aplicação do padrão GoF de projeto estrutural Facade.
Aplicação do padrão GoF de projeto estrutural Flyweight.
Aplicação do padrão GoF de projeto estrutural Proxy.
Podcast
Ouça o podcast. Com ele, encerramos o nosso estudo dos padrões GoF estruturais.
Conteúdo interativo
Acesse a versão digital para ouvir o áudio.
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.
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 GoF estruturais
1. Itens iniciais
Preparação
Objetivos
Introdução
Conteúdo interativo
1. Padrão de projeto Adapter
Intenção e problema resolvido pelo padrão Adapter
Conteúdo interativo
Intenção do padrão adapter
Problema resolvido pelo padrão Adapter
Atividade 1
Solução, consequências e padrões relacionados ao padrão Adapter
Conteúdo interativo
Solução do padrão Adapter
Comentário
Comentário
Consequências e padrões relacionados ao padrão Adapter
Comentário
Atividade 2
2. Padrões de projeto Bridge e Decorator
Intenção e problema resolvido pelo padrão Bridge
Conteúdo interativo
Intenção do padrão Bridge
Problema resolvido pelo padrão Bridge
Exemplo
E qual problema se apresenta agora?
Atividade 1
Solução, consequências e padrões relacionados ao padrão Bridge
Conteúdo interativo
Solução do padrão Bridge
Atenção
Consequências e padrões relacionados ao padrão Bridge
Atividade 2
Intenção e problema resolvido pelo padrão Decorator
Conteúdo interativo
Intenção do padrão Decorator
Problema resolvido pelo padrão Decorator
Atividade 3
Solução, consequências e padrões relacionados ao padrão Decorator
Conteúdo interativo
Solução do padrão Decorator
1
2
Atividade 4
3. Padrões de projeto Composite e Facade
Intenção e problema resolvido pelo padrão Composite
Conteúdo interativo
Intenção do padrão Composite
Problema resolvido pelo padrão Composite
Atividade 1
Solução, consequências e padrões relacionados ao padrão Composite
Conteúdo interativo
Solução do padrão Composite
Consequências e padrões relacionados ao padrão Composite
Atividade 2
Intenção e problema resolvido pelo padrão Facade
Conteúdo interativo
Intenção do padrão Facade
Problema resolvido pelo padrão Facade
Atividade 3
Solução, consequências e padrões relacionados ao padrão Facade
Conteúdo interativo
Solução do padrão Facade
Consequências e padrões relacionados ao padrão Facade
Padrão Abstract Factory
Padrão Mediator
Padrão Facade
Atividade 4
4. Padrões de projeto Flyweight e Proxy
Intenção e problema resolvido pelo padrão Flyweight
Conteúdo interativo
Intenção do padrão Flyweight
Problema resolvido pelo padrão Flyweight
Classe SubstanciaSimples
Classe SubstanciaComposta
Comentário
Atividade 1
Solução, consequências e padrões relacionados ao padrão Flyweight
Conteúdo interativo
Solução do padrão Flyweight
ConcreteFlyweight
UnsharedConcreteFlyweight
Comentário
Consequências e padrões relacionados ao padrão Flyweight
Exemplo
Atividade 2
Intenção e problema resolvido pelo padrão Proxy
Conteúdo interativo
Intenção do padrão Proxy
Problema resolvido pelo padrão Proxy
Atividade 3
Solução, consequências e padrões relacionados ao padrão Proxy
Conteúdo interativo
Solução do padrão Proxy
Remote proxy
Virtual proxy
Interceptor proxy
Consequências e padrões relacionados ao padrão Proxy
Atividade 4
5. Conclusão
Considerações finais
O que você aprendeu neste conteúdo?
Podcast
Conteúdo interativo
Explore +
Referênciasno mercado. Compare as soluções:
Perceba que, além de nomes distintos, os parâmetros dessas operações também são diferentes.
java
// 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.
1.
2.
3.
4.
Solução de pagamento 1
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).
Solução de pagamento 2
A segunda solução fornece a classe
ABCPagamentos com apenas uma
operação de pagamento.
java
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();
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.
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.
Atividade 1
Assinale a alternativa que expressa a intenção do padrão de projeto Adapter:
A
Permitir a utilização de objetos remotos como se, tanto o objeto remoto chamado como o objeto chamador,
estivessem rodando no mesmo processo.
B
Permitir a utilização de diferentes implementações de um mesmo serviço a partir de uma interface comum,
sendo que essas implementações correspondem normalmente a componentes fornecidos por terceiros.
C
Permitir a adição de funcionalidades a objetos existentes sem utilizar herança.
D
Permitir a utilização racional de memória evitando a criação de um número muito grande de objetos simples.
E
Oferecer uma interface de alto nível para acesso a um subsistema.
A alternativa B está correta.
O padrão Adapter define uma estrutura de adaptadores que convertem uma interface comum em uma
interface fornecida por uma implementação específica, tipicamente fornecida por terceiros. As alternativas
A, C, D e E correspondem, respectivamente, aos padrões Proxy, Decorator, Flyweight e Facade.
Solução, consequências e padrões relacionados ao padrão
Adapter
O padrão Adapter resolve a incompatibilidade entre interfaces, permitindo que classes com interfaces
diferentes trabalhem juntas ao adaptar uma interface existente para a interface que o cliente espera. As
vantagens incluem maior reutilização de código e flexibilidade na integração de sistemas, embora possa
aumentar a complexidade do código.
Padrões relacionados incluem o Bridge, que também separa abstração de implementação, e o Facade, que
fornece uma interface simplificada para um subsistema complexo. Enquanto o Adapter se concentra na
compatibilidade de interfaces, o Bridge e o Facade lidam com a organização e simplificação de sistemas
complexos.
Neste vídeo, você vai compreender uma solução para o problema resolvido pelo padrão Adapter, permitindo
que classes com interfaces incompatíveis trabalhem juntas, adaptando uma interface existente para a
interface esperada pelo cliente, promovendo reutilização de código e flexibilidade, bem como as
consequências e os padrões relacionados.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
Solução do padrão Adapter
O Adapter é um padrão cuja estrutura é apresentada no diagrama UML. Vejamos a seguir:
Estrutura do padrão Adapter.
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 seguir veremos a aplicação desse padrão no exemplo dos fornecedores de API de pagamentos:
Aplicação do padrão Adapter para o exemplo dos fornecedores de API de
pagamentos.
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.
java
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);
}
}
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.
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.
O padrão Bridge tem uma estrutura similar ao padrão Adapter, porém, com um objetivo diferente. Veja a
comparação dos objetivos de cada um:
Atividade 2
Em relação à estrutura de solução proposta pelo padrão de projeto Adapter, assinale a alternativa correta:
A
O participante Adaptee adapta a interface genérica definida pelo Adapter às operações definidas na interface
específica do objeto alvo definido pelo participante Target.
B
As operações definidas no participante Adapter possuem a mesma assinatura das operações definidas no
participante Adaptee.
C
O participante Adapter possui a responsabilidade de converter as operações definidas pelo participante
Target nas operações definidas no participante Adaptee.
Bridge
O padrão Bridge separa 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.
Adapter
O padrão Adapter, por sua vez, visa
oferecer uma diferente interface para
um ou mais componentes existentes,
tipicamente fornecidos por terceiros.
D
O participante Target representa cada implementação específica do serviço para a qual deve ser
implementada um adaptador.
E
As operações definidas no participante Target possuem a mesma assinatura das operações definidas no
participante Adaptee.
A alternativa C está correta.
No padrão Adapter, Target define uma interface genérica comum que os clientes deverão utilizar. Adaptee
corresponde a uma implementação específica de uma interface definida pelo fornecedor do componente. O
participante Adapter é responsável por adaptar as chamadas à interface genérica (Target) para a API
específica (Adaptee). Portanto, as operações definidas em cada Adapter devem possuir a mesma
assinatura daquelas definidas no participante Target.
2. Padrões de projeto Bridge e Decorator
Intenção e problema resolvido pelo padrão Bridge
O padrão Bridge visa desacoplar uma abstração de sua implementação para que ambas possam variar
independentemente. Ele resolve o problema da proliferação de subclasses que ocorre quando é necessário
combinar múltiplas abstrações com múltiplas implementações.
Em vez de criar subclasses para cada combinação, o Bridge separa as hierarquias de abstração e
implementação, conectando-as por uma "ponte". Isso resulta em um design mais flexível, reduz a
complexidade e facilita a manutenção e evolução do sistema, permitindo mudanças nas abstrações e
implementações sem que uma afete a outra.
Neste vídeo, você vai entender a intenção e o problema resolvido pelo padrão Bridge, que separa a abstração
da implementação, permitindo que elas variem independentemente, promovendo maior flexibilidade,
extensibilidade e desacoplamento entre as partes do sistema.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
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.
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 outra para Motif. Você consegue perceber que o aspecto de
variação entre as duas subclasses de TextComponent é a plataforma?
Exemplo de componente textual com implementação distinta para Windows e Motif.
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.
Exemplo de variações do componente textual inerentes ao domínio da abstração.
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?
E qual problema se apresenta agora?
O problema agora é que tanto um TextField quanto um TextArea precisam ser implementados nas duas
plataformas: Windows e Motif.
Assim, o mais adequado seria uma solução baseada na combinação dessas classificações, como a ilustrada
no diagrama UML a seguir:
Solução baseada na combinação de classificações.
Perceba que esse caminho de solução gera uma enormecombinaçã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 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.
Atividade 1
Assinale a alternativa que expressa a intenção do padrão de projeto Bridge:
A
Fornecer uma ponte entre dois objetos distribuídos, permitindo que a chamada de uma operação ao objeto
local seja redirecionada para o objeto remoto.
B
Permitir adicionar novas funcionalidades a um objeto por meio da composição dinâmica de objetos, em vez de
utilizar subclasses.
C
Fornecer uma ponte para um subsistema por meio de uma interface de alto nível de abstração, evitando que
um módulo cliente tenha que conhecer os detalhes internos da estrutura desse subsistema.
D
Garantir que exista uma (e apenas uma) instância de uma classe, provendo um ponto de acesso global a essa
instância.
E
Permitir que uma abstração e a sua representação interna ou tecnológica possam variar de forma
independente, fornecendo uma ponte entre a abstração e a sua representação.
A alternativa E está correta.
A alternativa A está incorreta, pois o padrão Bridge não está relacionado a redirecionamento de chamadas
entre objetos distribuídos, objetivo do padrão Proxy. A alternativa B está incorreta, pois esse objetivo está
relacionado ao padrão Decorator. A alternativa C está incorreta, pois oferecer uma interface de alto nível
para um subsistema é objetivo do padrão Facade. A alternativa D está incorreta, pois este é o objetivo do
padrão Singleton. A alternativa E está correta, pois o padrão Bridge permite que uma abstração e as suas
possíveis representações sejam representadas de forma separada, provendo uma ligação que permite que
a abstração implemente uma operação utilizando os recursos disponíveis na sua representação.
Solução, consequências e padrões relacionados ao padrão
Bridge
A solução do padrão Bridge envolve separar a abstração da implementação, permitindo que ambas evoluam
independentemente. Isso é feito através da criação de uma interface que atua como uma ponte entre a
abstração e suas implementações concretas. As consequências incluem maior flexibilidade e extensibilidade,
simplificação do código e redução da proliferação de subclasses. No entanto, pode aumentar a complexidade
inicial do design.
Padrões relacionados incluem Adapter, que adapta interfaces para compatibilidade; Composite, que trata
objetos individuais e composições uniformemente; e Strategy, que encapsula algoritmos para
intercambialidade. Todos promovem a separação de preocupações e maior modularidade.
No vídeo, você vai entender a solução para o problema resolvido pelo padrão Bridge, que separa a abstração
da implementação, permitindo que elas variem independentemente, promovendo maior flexibilidade,
extensibilidade e desacoplamento entre as partes do sistema, bem como as consequências e padrões
relacionados.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
Solução do padrão Bridge
A estrutura da solução proposta pelo padrão Bridge está representada no diagrama de classes a seguir:
Estrutura do padrão Bridge.
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.
Padrão Bridge aplicado no Java AWT.
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.
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, ainda podemos perceber o seguinte sobre os componentes TextArea e TextAreaPeer:
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:
java
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);
}
•
•
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.
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.
A estrutura de solução do padrão Bridge é similar à do padrão Adapter, mas a diferença fundamental é a
seguinte:
Atividade 2
Você e seus colegas de equipe estão conversando sobre padrões que poderiam ser adotados em determinado
projeto. Um de seus colegas sugeriu padrão Bridge de GoF, apresentando várias vantagens. Mas dentre as
opções a seguir, qual é a principal vantagem proporcionada pelo padrão Bridge nos padrões estruturais GoF?
Marque a resposta correta.
A
Facilita a integração de classes com interfaces incompatíveis.
B
Permite adicionar novas funcionalidades a objetos dinamicamente.
C
Reduza complexidade do código ao separar a lógica de negócio da interface do usuário.
D
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. 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.
Bridge
Esse padrão, por outro lado, é uma
solução definida a priori. Isso significa
que 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.
Imagem representativa da criptografia de textos.
Evita a necessidade de criar múltiplas subclasses para cada combinação de abstração e implementação.
E
Simplifica a interação com subsistemas complexos através de uma interface única.
A alternativa D está correta.
O padrão Bridge permite evitar a criação de múltiplas subclasses para cada combinação de abstração e
implementação, desacoplando esses dois aspectos e promovendo um design mais flexível e extensível. Isso
reduz a complexidade do sistema, facilitando a manutenção e evolução ao longo do tempo.
Intenção e problema resolvido pelo padrão Decorator
O padrão Decorator tem como intenção adicionar responsabilidades a objetos de modo dinâmico e
transparente, sem alterar sua estrutura básica. Ele resolve o problema de estender funcionalidades de forma
flexível, permitindo que objetos possam ser envolvidos por uma ou mais camadas de decorators, que
adicionam comportamentos adicionais. Isso é especialmente útil quando subclasses estáticas gerariam uma
grande quantidade de subclasses para cada combinação de funcionalidades adicionais.
O Decorator permite combinações flexíveis de comportamentos através da composição, mantendo o código
limpo e modular, enquanto permite a adição e remoção de funcionalidades de maneira dinâmica durante a
execução do programa.
No vídeo, você entenderá a intenção e o problema resolvido pelo padrão Decorator, que permite adicionar
comportamento ou responsabilidades a objetos individualmente, de forma dinâmica, sem afetar outros objetos
da mesma classe, promovendo flexibilidade e reutilização de código.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
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.
Imagine uma situação em 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.
Esse pacote 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, criando uma
estrutura como a ilustrada no código a seguir.
java
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, sobrescrevendo os métodos write e close. Nessa
solução simplificada, supomos que a compactação ocorre somente no momento do fechamento do arquivo.
java
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();
}
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. Veja a seguir o que
poderia ser feito e a situação que seria criada:
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.
Atividade 3
Assinale a alternativa que expressa a intenção do padrão de projeto Decorator:
A
Permitir a utilização mais racional de memória por meio do compartilhamento de objetos.
B
Adicionar funcionalidades a uma classe sem utilizar subclasses, mas por meio de uma estrutura de
composição dinâmica e flexível.
C
Fornecer uma interface de alto nível para um subsistema ou componente.
D
Permitir a utilização de diferentes implementações de um serviço fornecida por terceiros e que não podem ser
modificadas, por meio da definição de uma interface comum e de elementos que fazem a tradução dessa
interface comum para as interfaces específicas fornecidas pelos terceiros.
E
Representar hierarquias de composição de objetos, de forma que tanto os objetos individuais como os
agregados possam ser gerenciados de maneira uniforme pelo mesmo conjunto de operações.
CompressedFileWriter como subclasse
Poderíamos definir a classe
CompressedFileWriter como uma subclasse
da classe EncryptedFileWriter, em vez de
FileWriter.
Situação problemática
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.
A alternativa B está correta.
A alternativa A está incorreta, pois corresponde ao objetivo do padrão Proxy. A alternativa C está incorreta,
pois oferecer uma interface de alto nível para um subsistema é objetivo do padrão Facade. A alternativa D
está incorreta, pois esse é o objetivo do padrão Adapter. A alternativa E está incorreta, pois esse é o
objetivo do padrão Composite.
Solução, consequências e padrões relacionados ao padrão
Decorator
O padrão Decorator resolve o problema de estender objetos dinamicamente adicionando responsabilidades
sem modificar sua estrutura original. Isso é alcançado com a composição de objetos em várias camadas de
decorators, cada um adicionando funcionalidades específicas. As consequências incluem a promoção da
flexibilidade e da reutilização de código, permitindo combinações variadas de comportamentos. Pode
aumentar a complexidade ao introduzir muitas classes pequenas e aumentar o número de objetos
encapsulados.
Padrões relacionados incluem Adapter, que também modifica interfaces; e Composite, que trata objetos
individuais e composições uniformemente, ambos promovendo a modularidade e a extensibilidade de
sistemas complexos.
Nestevídeo, você entenderá a solução para o problema resolvido pelo padrão Decorator, que permite
adicionar comportamento ou responsabilidades a objetos individualmente, de forma dinâmica, sem afetar
outros objetos da mesma classe, promovendo flexibilidade e reutilização de código, bem como as
consequências e padrões relacionados.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
Solução do padrão Decorator
A estrutura da solução proposta pelo padrão Decorator está representada no diagrama de classes a seguir:
Estrutura do padrão Decorator.
Agora, vamos observar o papel dos participantes da estrutura apresentados 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.
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:
Solução do padrão Decorator utilizada em classes do pacote Java I/O.
Agora, vamos entender o papel das classes apresentadas a seguir:
Writer é uma classe abstrata que corresponde ao participante Component do padrão.
Já 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. Veja o que
•
•
•
•
•
•
acontece com as classes a seguir:
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 adiante 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.
java
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 adiante mostra como essas classes podem ser utilizadas em conjunto. Devemos notar que:
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. Veja o código:
java
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);
}
}
}
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. Entenda essa flexibilidade a
seguir:
1
Com a utilização do padrão Decorator, responsabilidades podem ser adicionadas, em tempo de
execução, pela adição de camadas de decoradores, formando uma estrutura similar a uma cebola.
2
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
frequentemente a superclasse tem que ser estendida para comportar novas operações definidas nas
subclasses.
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 com a seguinte diferença:
•
•
Será que você consegue implementar as classes EncryptedFileWriter e CompressedFileWriter apresentadas
anteriormente utilizando o padrão Decorator?
Atividade 4
Imagine ser convidado a participar de um podcast sobre tecnologia e ser questionado sobre padrões GoF
estruturais, mais especificamente sobre o padrão Decorator. Dentre as opções, qual é a principal solução
oferecida pelo padrão Decorator nos padrões estruturais GoF? Marque a resposta correta.
A
Facilita a integração de classes com interfaces incompatíveis.
B
Adiciona novas funcionalidades a objetos dinamicamente sem alterar sua estrutura.
C
Simplifica a interação com subsistemas complexos através de uma interface única.
D
Separa a abstração de sua implementação, permitindo que ambas possam variar independentemente.
E
Reduz a complexidade do código ao eliminar a necessidade de subclasses para cada combinação de
funcionalidades.
A alternativa B está correta.
O padrão Decorator permite adicionar responsabilidades a objetos dinamicamente durante a execução do
programa, sem modificar sua estrutura original. Isso é alcançado pela composição de objetos com
decorators que adicionam funcionalidades específicas. Essa flexibilidade oferecida pelo Decorator é
fundamental para estender objetos de forma modulare reutilizável, evitando a criação de subclasses para
cada nova combinação de funcionalidades, o que promove um design mais flexível e fácil de manter.
Decorator
Um decorator tem o propósito de adicionar
responsabilidades a outros objetos.
Composite
O padrão Composite está mais ligado à
agregação de objetos em uma
estrutura hierárquica.
Desenvolvimento de programa de mensagens
organizadas em pastas.
3. Padrões de projeto Composite e Facade
Intenção e problema resolvido pelo padrão Composite
O padrão Composite tem como intenção compor objetos em estruturas de árvore para representar hierarquias
parte-todo. Ele resolve o problema de tratar tanto objetos individuais quanto composições de objetos de
maneira uniforme, permitindo que clientes usem recursivamente estruturas complexas como se fossem
objetos individuais. Isso simplifica o código ao não exigir distinções entre objetos simples e compostos ao
manipular a estrutura.
O Composite é ideal para representar estruturas hierárquicas em que partes podem ser tratadas de modo
uniforme, promovendo a reutilização de código e a clareza na manipulação de estruturas complexas dentro de
um sistema.
Neste vídeo, você vai entender a intenção e o problema resolvido pelo padrão Composite, que permite tratar
objetos individuais e composições de objetos de maneira uniforme, organizando objetos em estruturas
hierárquicas para representar partes e todo.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
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
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.
java
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).
A adição de novas operações e novos tipos de objeto, certamente, aumentará a complexidade dessa
implementação.
Atividade 1
Assinale a alternativa que expressa a intenção do padrão de projeto Composite:
A
Compor hierarquias de objetos de forma que objetos individuais e agregados possam ser manipulados de
forma genérica pelo mesmo conjunto de operações.
B
Permitir a composição de objetos a partir de diversos pequenos objetos imutáveis, por meio de uma solução
baseada em compartilhamento.
C
Permitir a instanciação de objetos a partir da geração da cópia de um objeto, fazendo com que o módulo
cliente não precise conhecer a classe específica que está sendo instanciada.
D
Adicionar novas funcionalidades a um objeto, por meio da composição aninhada de objetos.
E
Converter a interface de um componente fornecido por terceiros, cujo código-fonte não pode ser alterado, em
uma interface comum esperada pelos módulos clientes de uma aplicação.
A alternativa A está correta.
A alternativa B está incorreta, pois o compartilhamento de objetos imutáveis está relacionado com o padrão
Flyweight. As alternativas C, D e E correspondem, respectivamente, aos padrões Clone, Decorator e
Adapter.
Solução, consequências e padrões relacionados ao padrão
Composite
O padrão Composite resolve o problema de tratar objetos individuais e composições de objetos de maneira
uniforme, utilizando uma estrutura em árvore. Isso é alcançado através da definição de uma interface comum
para elementos simples e compostos. As consequências incluem simplificação do código ao manipular
hierarquias complexas, facilitando a adição de novos tipos de elementos. Pode aumentar a complexidade ao
requerer tratamento especial para elementos que não suportam todas as operações.
Padrões relacionados incluem Decorator, que adiciona responsabilidades a objetos dinamicamente; e Iterator,
que permite acessar elementos de uma coleção sem expor sua estrutura interna, ambos promovendo a
manipulação flexível e eficiente de estruturas de dados complexas.
Neste vídeo, você vai entender solução para o problema resolvido pelo padrão Composite, que permite tratar
objetos individuais e composições de objetos de maneira uniforme, organizando objetos em estruturas
hierárquicas para representar partes e todo, bem como as consequências e padrões relacionados.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
Solução do padrão Composite
A estrutura da solução proposta pelo padrão Composite está representada no diagrama de classes a seguir:
Estrutura do padrão Composite.
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. Veja o que representam os elementos a
seguir:
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:
•
•
•
•
•
java
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 {
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 (Elementoelemento : 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
}
}
Veja, a seguir, a que correspondem alguns elementos e como eles funcionam:
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.
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.
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.
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.
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 padrão 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. Veja a seguir uma vantagem e uma desvantagem desse padrão:
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.
Atividade 2
Imagine que você esteja trabalhando em uma empresa de desenvolvimento de software e precise decidir qual
padrão adotar para determinado projeto. Em conversa com um colega de equipe, ele mencionou algumas
características do padrão Composite. Entre as opções, qual é a principal solução oferecida pelo padrão
Composite nos padrões estruturais GoF?
A
•
•
•
•
•
Vantagem
O padrão permite adicionar novos tipos de
componentes sem que seja necessário
alterar o código dos módulos clientes.
Desvantagem
Sua 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.
Converter a interface de uma classe em outra interface que o cliente espera.
B
Permitir que objetos sejam compostos em estruturas de árvore para representar hierarquias parte-todo.
C
Separar a abstração de sua implementação, permitindo que ambas possam variar independentemente.
D
Adicionar funcionalidades a objetos dinamicamente sem modificar sua estrutura original.
E
Facilitar a interação com subsistemas complexos através de uma interface única.
A alternativa B está correta.
O padrão Composite permite que objetos sejam organizados em estruturas hierárquicas, em que cada
elemento pode ser tratado uniformemente, independentemente se é um objeto simples ou uma composição
de objetos. Isso simplifica o código ao manipular estruturas complexas como se fossem objetos individuais,
promovendo a reutilização de código e facilitando a representação de relações parte-todo dentro de um
sistema.
Intenção e problema resolvido pelo padrão Facade
O padrão Facade tem como intenção fornecer uma interface simplificada e unificada para um conjunto de
interfaces em um subsistema, tornando esse subsistema mais fácil de usar. Ele resolve o problema da
complexidade ao lidar com sistemas que possuem muitas interações ou dependências entre classes,
proporcionando uma única interface de alto nível que reduz a complexidade e facilita o uso.
O Facade oculta os detalhes de implementação e fornece uma interface mais clara e simples, permitindo que
os clientes interajam com o sistema sem precisar entender toda a sua complexidade interna, melhorando a
modularidade e a manutenibilidade.
Neste vídeo, você entenderá a intenção e o problema resolvido pelo padrão Facade, que permite fornecer
uma interface simplificada para um conjunto de interfaces em um subsistema complexo, facilitando a
utilização e promovendo um baixo acoplamento entre os clientes e o subsistema.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
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.
Imagem representativa do comércio eletrônico.
Problema resolvido pelo padrão Facade
Imagine que você esteja implementando um
sistema de comércio eletrônico e que esse
sistema possa 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 distintas.
Nessa separação, em uma camada ficariam os
elementos que compõem a interface com o
usuário; em outra, a lógica do negócio; e por
fim, 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
um objeto para calcular o prazo de entrega
A estrutura de dependências resultante dessa solução é apresentada no diagrama de classes a seguir:
Estrutura de dependências para implementação da classe PaginaCompras.
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.
•
•
•
•
•
•
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.
Atividade 3
Assinale a alternativa que expressa a intenção do padrão de projeto Facade:
A
Fornecer uma interface para a interação com um objeto remoto, oferecendo transparência de localização, isto
é, os módulos chamadores podem se comunicar com o objeto remoto como se ele estivesse rodando
localmente.
B
Reduzir o acoplamento de um módulo clientecom os elementos que compõem um subsistema, fornecendo
para o módulo cliente uma interface de alto nível aos serviços desse subsistema.
C
Evitar a instanciação de uma grande quantidade de objetos imutáveis, por meio de uma solução baseada em
compartilhamento de objetos.
D
Representar hierarquias de composição de objetos, de forma que tanto os objetos individuais quanto os
agregados possam ser gerenciados pelo mesmo conjunto de operações.
E
Desacoplar uma abstração das suas possíveis representações, permitindo que tanto a abstração quanto as
suas representações possam evoluir de forma independente.
A alternativa B está correta.
As alternativas A, C, D e E correspondem, respectivamente, aos objetivos dos padrões Proxy, Flyweight,
Composite e Bridge.
Solução, consequências e padrões relacionados ao padrão
Facade
O padrão Facade fornece uma solução ao criar uma interface simplificada e unificada para um conjunto de
interfaces complexas em um subsistema, facilitando seu uso. As consequências incluem a redução da
complexidade percebida pelos clientes, melhorando a facilidade de uso e a compreensão do sistema. No
entanto, pode esconder funcionalidades mais específicas que ainda podem ser necessárias para alguns
clientes.
Padrões relacionados incluem Adapter, que também altera interfaces para compatibilidade; e Mediator, que
centraliza a comunicação entre objetos para reduzir dependências diretas. Ambos, assim como o Facade,
visam simplificar interações e melhorar a modularidade e a manutenibilidade do sistema.
Neste vídeo, você entenderá a solução para o problema resolvido pelo padrão Facade, que permite fornecer
uma interface simplificada para um conjunto de interfaces em um subsistema complexo, facilitando a
utilização e promovendo um baixo acoplamento entre os clientes e o subsistema, bem como as consequências
e padrões relacionados.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
Solução do padrão Facade
A estrutura da solução proposta pelo padrão Facade é apresentada no diagrama UML adiante.
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.
Estrutura do padrão Facade.
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.
Aplicação do padrão Facade com interação entre PaginaCompras e
ServicoCompras.
Consequências e padrões relacionados ao padrão 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. Veja a
seguir algumas possibilidades de uso com outros padrões:
Padrão Abstract Factory
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.
Padrão Mediator
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.
Padrão Facade
O padrão Facade tem o propósito de fornecer uma interface de alto nível para clientes de outros
subsistemas.
Atividade 4
Imagine que você esteja assistindo a uma palestra sobre padrões utilizados no desenvolvimento de software.
No intervalo, você e alguns colegas conversam sobre o padrão Facade. Considere que as opiniões dos
colegas estão descritas nas opções a seguir. Qual é a principal solução oferecida pelo Facade nos padrões
estruturais GoF? Marque a resposta correta.
A
Adicionar funcionalidades a objetos dinamicamente sem modificar sua estrutura original.
B
Converter a interface de uma classe em outra interface que o cliente espera.
C
Fornecer uma interface simplificada e unificada para um conjunto de interfaces em um subsistema complexo.
D
Permitir que objetos sejam compostos em estruturas de árvore para representar hierarquias parte-todo.
E
Separar a abstração de sua implementação, permitindo que ambas possam variar independentemente.
A alternativa C está correta.
O padrão Facade cria uma interface simplificada e unificada para um conjunto de interfaces complexas em
um subsistema. Isso reduz a complexidade percebida pelos clientes, facilitando o uso e a compreensão do
sistema sem a necessidade de entender todos os detalhes internos. Facade torna a interação com sistemas
complexos mais direta e manejável, promovendo uma melhor modularidade e manutenibilidade.
Implementação de uma aplicação que envolve
substâncias químicas.
4. Padrões de projeto Flyweight e Proxy
Intenção e problema resolvido pelo padrão Flyweight
O padrão Flyweight tem a intenção de minimizar o uso de memória compartilhando o máximo possível de
dados entre múltiplos objetos semelhantes. Ele resolve o problema do alto consumo de memória em sistemas
que necessitam de muitos objetos de granularidade fina, especialmente quando muitos desses objetos são
quase idênticos.
O Flyweight separa os dados intrínsecos (compartilháveis) dos dados extrínsecos (específicos de cada objeto)
e armazena apenas uma instância dos dados intrínsecos. Isso permite que muitos objetos reutilizem os
mesmos dados intrínsecos, reduzindo significativamente a quantidade de memória necessária para
representar um grande número de objetos.
Neste vídeo, você vai entender a intenção e o problema resolvido pelo padrão Flyweight, que permite
minimizar o uso de memória compartilhando o máximo de dados possível entre objetos semelhantes,
promovendo eficiência e reduzindo a sobrecarga de criação de muitos objetos semelhantes.
Conteúdo interativo
Acesse a versão digital para assistir ao vídeo.
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.
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 o
hidrogênio e o enxofre .
Uma substância composta é formada por
átomos de elementos químicos diferentes como
a água e - bicarbonato de sódio
.
Uma maneira de representar esses elementos
em Java é expressa no código a seguir:
java
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;