Skip to content

Padrões estruturais

Os padrões estruturais explicam como construir objetos e classes em estruturas maiores, mantendo elas flexíveis e eficientes. Alguns dos padrões compreendidos aqui são:

  • Adapter: permite criar objetos com interfaces incompatíveis possam colaborar;
  • Bridge: permite criar uma classe grande em classes menores relacionadas com implementações que podem ser desenvolvidas de forma independente uma da outra;
  • Composite: permite criar arvores de estrutura e trabalhar com elas como se fossem objetos individuais;
  • Decorator: permite adicionar novos comportamentos a objetos colocando eles em objetos especiais que contem esses comportamentos;
  • Facade: traz uma interface simplificada para um conjunto complexo de classes;
  • Flyweight: permite ajustar mais objetos em memória RAM, compartilhando recursos comuns entre eles, mantendo apenas os dados de cada objeto;
  • Proxy: permite criar um substituto para outro objeto. Um proxy permite criar acesso ao objeto original, permitindo modificar os elementos antes ou depois da requisição ao objeto.

Legal, agora que temos uma definição geral sobre os padrões estruturais, vamos ver como utilizar eles em nossos projetos e como conseguimos resolver diferentes problemas com sua aplicação.

Para nossa aplicação, vamos considerar um mini editor de desenho que renderiza formas e imagens na tela (importante: não serão renderizadas imagens efetivamente, só a critério de demonstração). Vamos verificar como poderia ser esse código:

MainBad.java
import java.util.*;
//CanvasBad.java
class CanvasBad {
private List<Object> items = new ArrayList<>(); // Sem tipo claro
public void add(Object o){ items.add(o); }
// Renderização toda em if/else - difícil de escalar
public void render(){
for(Object o : items){
if(o instanceof CircleBad){
CircleBad c = (CircleBad)o;
System.out.println("Desenhando Círculo com raio="+c.radius+" em modo RASTER fixo");
} else if(o instanceof RectBad){
RectBad r = (RectBad)o;
System.out.println("Desenhando Retângulo w="+r.w+" h="+r.h+" em modo RASTER fixo");
} else if(o instanceof HeavyImageBad){
HeavyImageBad img = (HeavyImageBad)o;
// Sempre carrega pesado, mesmo sem precisar
img.load();
System.out.println("Mostrando imagem pesada: "+img.path);
}
}
}
}
//CircleBad.java
class CircleBad { public final int radius; public CircleBad(int r){ this.radius=r; } }
//RectBad.java
class RectBad { public final int w,h; public RectBad(int w,int h){ this.w=w; this.h=h; } }
//HeavyImageBad.java
class HeavyImageBad {
public final String path; public HeavyImageBad(String p){ this.path=p; }
public void load(){ System.out.println("[LOAD] Carregando bitmap enorme de "+path); }
}
//MainBad.java
public class MainBad {
public static void main(String[] args){
CanvasBad c = new CanvasBad();
c.add(new CircleBad(10));
c.add(new RectBad(20, 15));
c.add(new HeavyImageBad("/imgs/bg-huge.png"));
c.render();
}
}

Esse padrão também é conhecido como Adaptador ou Wrapper. Seu objetivo é possibilitar que objetos que não possuem interfaces semelhantes possam colaborar entre si.

O refactoring.guru traz um exemplo que eu considero muito interessante aqui para compreendermos o problema que o adapter tenta resolver. Vamos pensar na seguinte situação: você tem uma aplicação que recebe dados no formato XML de um provedor (pense em uma aplicação que coleta dados de preços de ações). A aplicação consegue receber esses dados e processar eles para a aplicação. Agora, para expandir a aplicação, precisamos mandar os dados para uma outra aplicação, mas ela recebe apenas os dados no formato JSON. E agora o que fazer?

Uma abordagem que poderia resolver esse problema é criar um adaptador. Um adaptador é um objeto especial que converte a interface de um objeto para que outro objeto possa compreender ele. O processo consiste em esconder a complexidade da conversão acontecendo nos bastidores da aplicação. Desta forma, nenhum dos dois objetos que estão trocando informação tem conhecimento do processo de adequação que está acontecendo.

O seu fluxo de utilização é o seguinte:

  1. O adaptador obtém uma interface compatível com os objetos já existentes;
  2. Utilizando essa interface, o objeto existente pode chamar os métodos do adaptador;
  3. Quando receber uma chama, o adaptador passa o pedido para segundo objeto, mas em um formato e ordem que o segundo objeto espera receber.
  • Em alguns casos é possível ou necessário criar um adaptador de duas vias que pode converter as chamadas em ambas as direções (de qualquer um dos objetos para qualquer outro objeto).

A implementação utiliza o princípio da composição de objetos, o adaptador implementa a interface de um objeto e encobre o outro. Desta forma, o código do cliente não é acoplado a interface implementada. Estes adaptadores podem ser expandidos, sem quebrar implementações que já foram realizadas. Uma outra alternativa é realizar sua implementação por herança.

Utilizar a classe Adaptador quando você quer utilizar uma classe existente, mas sua interface não for compatível com o resto do seu código. Utilize o padrão quando você quer reutilizar diversas subclasses existentes que não possuam alguma funcionalidade comum que não pode ser adicionada à superclasse. Isso evita código duplicado nas subclasses ou nas classes filhas, levando ele para a interface do adaptador.

Vamos alterar nosso exemplo base para adicionar nele o nosso padrão Adapter.

import java.util.*;
interface Drawable {
void draw();
}
class Circle implements Drawable {
private final int r;
public Circle(int r){
this.r = r;
}
@Override
public void draw(){
System.out.println("[Circle] r="+r);
}
}
class Rect implements Drawable {
public final int w,h;
public Rect(int w,int h){
this.w=w; this.h=h;
}
@Override
public void draw(){
System.out.println("[Rect] w="+ this.w + " h="+ this.h);
}
}
class LegacyImage {
private final String path;
public LegacyImage(String path){
this.path=path;
}
public void drawBitmap(){
System.out.println("[Legacy] drawBitmap: "+path);
}
}
class LegacyImageAdapter implements Drawable {
private final LegacyImage legacy;
public LegacyImageAdapter(LegacyImage legacy){
this.legacy = legacy;
}
@Override
public void draw(){
legacy.drawBitmap();
}
}
// Cliente que depende apenas da ABSTRAÇÃO (Drawable)
class Canvas {
private final List<Drawable> items = new ArrayList<>();
public void add(Drawable d){
items.add(d);
}
public void render(){
items.forEach(Drawable::draw); // <- Polimorfismo aqui
}
}
// Demonstração: objetos diferentes, mesma chamada .draw()
class Main {
public static void main(String[] args){
Canvas canvas = new Canvas();
canvas.add(new Circle(12)); // nativo
canvas.add(new Rect(3,4));
canvas.add(new LegacyImageAdapter(new LegacyImage("logo.bmp"))); // adaptado
canvas.render(); // chamadas .draw() são despachadas dinamicamente
}
}

Aqui podemos observar diversas coisas acontecendo ao mesmo tempo. Primeiro temos que localizar a oportunidade de utilizar o padrão. A classe LegacyImage tem um formato diferente do restante das classes que podem ser desenhados no sistema. Como o padrão adapter foi utilizado? Um adaptador para a classe foi construído levando em consideração uma interface que todas as outras classes implementavam. Repare em um detalhe de implementação, a classe LegacyImage não foi alterada. As modificações aconteceram no adaptador.

Vale destacar também, como ele trouxe uma oportunidade de melhoria para a construção do restante do código, que ficou mais enxuto e utilizando melhor o polimorfismo para sua representação.

O padrão Bridge permite separar grandes classes ou conjuntos de classes, em hierarquias intimamente ligadas, mas que a abstração e a implementação podem ser desenvolvidas de forma independente uma da outra. Pense agora em um conjunto de classes que representam formas geométricas e em um conjunto de classes que representam cores que essas formas podem assumir. O conjunto de combinações possível cresce de forma geométrica.

Seguindo esse exemplo, como podemos implementar nosso código? Essa é uma ótima pergunta! Podemos fazer essa implementação trazendo a relação entre as classes mais significativa. No exemplo acima, cada forma geométrica pode ser filhas da classe Forma, enquanto que a classe cor pode ter seus filhos como a class Cor.

Cada uma das partes recebe uma tarefa específica que deve ser realizada. Enquanto a abstração fornece a lógica de controle de alto nível para a o projeto, é o objeto de implementação que faz o trabalho por baixo dos panos.

Uma boa pedida para utilizar o padrão Bridge quando é necessário dividir e organizar uma classe monolítica que tem diversas formas de implementar uma mesma funcionalidade. Lembrem-se: quanto maior a classe, mais difícil é de compreender como ela funciona e mais complexo ainda é o tempo para fazer mudanças para ela.

Considere que uma hierarquia de classe existe quando uma dimensão da classe demanda um comportamento para ela. Isso significa que, quando um comportamento da classe pode ser construído com um outro conjunto de classes. Isso facilita muito as modificações e desacoplamento do projeto.

ATENÇÃO: O padrão Bridge é geralmente definido com antecedência, permitindo que as partes do sistema possam ser desenvolvidas de forma independente umas das outras. Já o Adapter é utilizado, em geral, quando as aplicações já existem.

Exemplo de utilização:

interface Renderer {
void drawCircle(int radius); void drawRect(int w,int h);
}
class VectorRenderer implements Renderer {
public void drawCircle(int radius){
System.out.println("[Vector] Circle r="+radius);
}
public void drawRect(int w,int h){
System.out.println("[Vector] Rect "+w+"x"+h);
}
}
class RasterRenderer implements Renderer {
public void drawCircle(int radius){
System.out.println("[Raster] Circle r="+radius);
}
public void drawRect(int w,int h){
System.out.println("[Raster] Rect "+w+"x"+h);
}
}
abstract class Shape {
protected final Renderer renderer;
protected Shape(Renderer r){ this.renderer=r; }
public abstract void draw();
}
class Circle extends Shape {
private final int radius;
public Circle(Renderer r,int radius){
super(r);
this.radius=radius;
}
public void draw(){
renderer.drawCircle(radius);
}
}
class Rect extends Shape {
private final int w,h;
public Rect(Renderer r,int w,int h){
super(r); this.w=w; this.h=h;
}
public void draw(){
renderer.drawRect(w,h);
}
}
class Main {
public static void main(String[] args){
Renderer vec = new VectorRenderer();
Renderer ras = new RasterRenderer();
new Circle(vec, 10).draw();
new Rect(ras, 20, 15).draw();
}
}

O Composite permite compor objetos em estruturas em árvore (parte–todo) e tratar objetos individuais e composições de maneira uniforme. A ideia é compor objetos em árvore (parte–todo) e permitir que cliente trate folhas e composições de forma uniforme.

Devemos utilizar quando temos hierarquias naturais (ex.: cena → grupo → sprite; pasta → subpastas → arquivos; menu → submenu → itens). Quando o cliente precisa chamar o mesmo método em nós simples e compostos: draw(), execute(), get_price(), etc. Regras de negócio devem propagar: “desabilitar um grupo desabilita tudo dentro”, “aplicar desconto ao pacote impacta os itens”.

Como benefícios temos:

  • Uniformidade de uso: reduz if isinstance(…) espalhados para diferenciar folha de nó.
  • Extensibilidade: adicionar novos tipos de componente exige menos mudanças no cliente.
  • Recursão elegante: operações agregadas (soma, média, render) fluem naturalmente.

Você precisa tratar indivíduos e grupos de forma uniforme (em Python, pense numa lista com elementos que também são listas recursivamente). Use quando precisar representar hierarquias (camadas, grupos, cenas) e quer que o cliente chame o mesmo método (draw(), execute(), etc.) tanto para elementos simples quanto para grupos.

import java.util.*;
interface Drawable { void draw(); }
// Folhas
class Circle implements Drawable {
private final int r;
public Circle(int r){this.r=r;}
public void draw(){ System.out.println("Circle r="+r); }
}
class Rect implements Drawable {
private final int w,h;
public Rect(int w,int h){this.w=w;this.h=h;}
public void draw(){
System.out.println("Rect "+w+"x"+h);
}
}
// Composto
class Group implements Drawable {
private final List<Drawable> children = new ArrayList<>();
public Group add(Drawable d){
children.add(d);
return this;
}
public void draw(){
children.forEach(Drawable::draw);
}
}
class Main {
public static void main(String[] args){
Group root = new Group()
.add(new Circle(10))
.add(new Rect(20,15));
Group layer = new Group().add(new Circle(5)).add(new Circle(7));
root.add(layer);
root.draw(); // desenha tudo
}
}

O Decorator adiciona comportamentos dinamicamente sem herança, “embrulhando” um objeto com outro que implementa a mesma interface. Ao adicionar responsabilidades dinamicamente embrulhando o objeto, sem herança explosiva. Podemos utilizar quando desejamos funcionalidades combináveis e opcionais, como: borda, sombra, cache, logging, compressão. Você quer evitar subclasses BordaSombraLoggingDrawable etc. Quando precisa alterar comportamento em runtime (ligar/desligar features).

Podemos determinar alguns benefícios:

  • Composição > herança: combina recursos em qualquer ordem.
  • Aberto/Fechado: novas responsabilidades sem tocar nas classes base.
  • Granularidade: cada decorator foca em uma única preocupação (ex.: só logging).

Quando quer “plugar” funcionalidades (borda, sombra, logging) em qualquer Drawable de forma combinável. Quando quiser combinar recursos opcionais (ex.: borda, sombra, cor, logging) sem explodir subclasses e mantendo abertura para composição em tempo de execução.

interface Drawable { void draw(); }
// Núcleo (component)
class Circle implements Drawable {
private final int r;
public Circle(int r){
this.r=r;
}
public void draw(){
System.out.println("Circle r="+r);
}
}
class Rect implements Drawable {
private final int w,h;
public Rect(int w, int h){
this.w = w;
this.h = h;
}
public void draw(){
System.out.println("Rect w="+this.w+" h="+ this.h);
}
}
// Decorator base
abstract class DrawableDecorator implements Drawable {
protected final Drawable inner;
protected DrawableDecorator(Drawable inner){
this.inner = inner;
}
}
// Concretos
class BorderDecorator extends DrawableDecorator {
public BorderDecorator(Drawable d){
super(d);
}
public void draw(){
System.out.println("+ Borda 1px");
inner.draw();
}
}
class ShadowDecorator extends DrawableDecorator {
public ShadowDecorator(Drawable d){
super(d);
}
public void draw(){
System.out.println("+ Sombra suave");
inner.draw();
}
}
public class Main {
public static void main(String[] args){
Drawable base = new Circle(12);
Drawable fancy = new BorderDecorator(new ShadowDecorator(base));
fancy.draw();
base = new Rect(3,4);
fancy = new ShadowDecorator(base);
fancy.draw();
}
}

O Facade fornece uma interface simplificada para um subsistema complexo, escondendo detalhes e passos internos. Desta forma, torna-se possível fornecer uma interface simples para um subsistema complexo (múltiplas classes/ordens de chamada). Devemos utilizar quando o cliente precisa de “um botão” para fluxos com muitos passos/objetos (pipeline de mídia, orquestração de APIs). Quando você quer diminuir acoplamento com detalhes internos que mudam com frequência. Ainda quando precisa impor ordem/protocolo correto de chamadas sem expor tudo.

Como benefícios:

  • Simplicidade para o cliente: reduz curva de aprendizado e erros de uso.
  • Encapsulamento de mudança: trocar bibliotecas internas impacta só a facade.
  • Ponto único de políticas: retries, timeouts, métricas ficam centralizados.

Você tem vários passos/objetos internos e quer expor uma API simples para o usuário (em Python, pense num módulo que encapsula detalhes feios). Quando o cliente não precisa conhecer várias classes/ordens de chamada internas e você quer entregar uma API coesa e fácil.

// Subsistema (oculto ao cliente)
class VectorEngine {
void line(){
System.out.println("[Vector] line()");
}
}
class RasterEngine {
void blit(){
System.out.println("[Raster] blit()");
}
}
class AssetLoader {
void load(String p){
System.out.println("[Asset] load "+p);
}
}
// Facade
class GraphicsFacade {
private final VectorEngine vector = new VectorEngine();
private final RasterEngine raster = new RasterEngine();
private final AssetLoader assets = new AssetLoader();
public void drawCircle(int r){
vector.line();
System.out.println("draw circle r="+r);
}
public void drawImage(String path){
assets.load(path);
raster.blit();
System.out.println("image: "+path);
}
}
// Cliente
class Main {
public static void main(String[] args){
GraphicsFacade g = new GraphicsFacade();
g.drawCircle(10);
g.drawImage("/imgs/logo.png");
}
}

Muitos objetos semelhantes consomem memória; parte do estado pode ser compartilhada (ex.: estilo/pen/cor). Em Python, lembre de interns de strings. Com ele, compartilhar estado intrínseco imutável entre muitos objetos parecidos para economizar memória; manter o estado extrínseco fora. É possível obter milhares/milhões de itens com grande parte de estado repetido (fonte, cor, textura, shape). Muitas vezes, a memória é gargalo (mapas, editores de texto, jogos com muitos tiles/partes).

Alguns benefícios para sua utilização:

  • Ordem(s) de grandeza menos objetos/bytes alocados.
  • Caches mais eficazes e menor pressão no Garbage-Collector.
// Estado intrínseco compartilhado
class ShapeStyle {
public final String stroke;
public final String fill;
private ShapeStyle(String stroke,String fill){
this.stroke=stroke;
this.fill=fill;
}
// Factory com cache
private static final java.util.Map<String,ShapeStyle> CACHE = new java.util.HashMap<>();
public static ShapeStyle of(String stroke,String fill){
String key = stroke+"|"+fill;
return CACHE.computeIfAbsent(key, k -> new ShapeStyle(stroke, fill));
}
}
// Objetos leves referenciam o estilo
class Circle {
private final int r;
private final ShapeStyle style;
public Circle(int r, ShapeStyle s){
this.r=r;
this.style=s;
}
public void draw(){
System.out.println("Circle r="+r+" style=("+style.stroke+","+style.fill+")");
}
}
class Main {
public static void main(String[] args){
var red = ShapeStyle.of("#f00","none");
var red2 = ShapeStyle.of("#f00","none");
System.out.println("Mesma instância? "+(red==red2)); // true
new Circle(5, red).draw();
new Circle(10, red2).draw();
}
}

Você quer lazy-load, caching, segurança ou acesso remoto sem mudar o cliente. Um “substituto” que controla o acesso ao objeto real para lazy-load, cache, segurança, controle remoto, logging. Ele permite a criação cara ou acesso remoto (banco, filesystem, serviço externo): carregue sob demanda. Possibilita também o Cross-cutting concerns locais ao acesso: cache de resultados, rate limiting, authz. Traz proteção: restringir operações sem mudar o cliente.

Como benefícios podemos citar:

  • Transparência para o cliente: mesma interface; alternar real/proxy não quebra código.
  • Performance/perfil de recursos: on-demand, cache, pooling.
  • Segurança e governança: ponto único para autenticar/autorizar/registrar.
// Sujeito real pesado
class HeavyImage implements Image {
private final String path;
private boolean loaded=false;
HeavyImage(String p){
this.path=p;
}
private void load(){
if(!loaded){
System.out.println("[LOAD] "+path);
loaded=true;
}
}
public void show(){
load();
System.out.println("show "+path);
}
}
// Interface comum
interface Image { void show(); }
// Proxy virtual: carrega sob demanda e faz cache
class ImageProxy implements Image {
private final String path;
private HeavyImage real;
public ImageProxy(String p){
this.path=p;
}
public void show(){
if(real==null){
real = new HeavyImage(path);
}
real.show();
}
}
// Cliente
class Main {
public static void main(String[] args){
Image img = new ImageProxy("/imgs/bg-huge.png");
System.out.println("— primeiro show —"); img.show();
System.out.println("— segundo show —"); img.show(); // Sem recarregar
}
}

8.1 Editor de Cena 2D (Composite + Decorator + Proxy)

Section titled “8.1 Editor de Cena 2D (Composite + Decorator + Proxy)”

Você está construindo um pequeno editor de cenas 2D para jogos. O usuário pode criar sprites, agrupá-los em camadas e grupos, e aplicar efeitos visuais (borda, sombra, opacidade) ativáveis/desativáveis em tempo de execução. As texturas de sprites vêm de disco ou URL e podem ser caras de carregar.

Tratar folhas e composições de forma uniforme (Composite). Adicionar comportamentos opcionais sem explodir subclasses (Decorator). Adiar carregamento de recursos e cachear acessos (Proxy).

  1. Composite
  • Interface Drawable com draw() e bounds().
  • Sprite (folha) e Group (composto).
  • Group permite add(child), remove(child) e chama draw() recursivamente.
  1. Decorator
  • Decorators Border, Shadow, Opacity implementam Drawable e embrulham outro Drawable.
  • Ordem de aplicação afeta o resultado (teste isso).
  • Deve ser possível empilhar decorators dinamicamente.
  1. Proxy
  • Texture real carrega imagem de uma origem (simule custo/latência).
  • TextureProxy com lazy-load no primeiro uso, cache em memória, e métricas (quantas cargas).
  • Caso de falha (simulada) deve ser tratado sem quebrar o editor (ex.: placeholder).

8.2 Editor de Texto Rico (Flyweight + Composite + Facade)

Section titled “8.2 Editor de Texto Rico (Flyweight + Composite + Facade)”

Você está implementando o núcleo de um editor de texto tipo “code/notes”. Cada caractere (glifo) tem estilo (fonte, tamanho, cor, peso). O documento tem seções e parágrafos. A exportação para PDF/HTML é um pipeline chato e com várias etapas.

Reduzir uso de memória por estilos repetidos (Flyweight). Representar documento como árvore e percorrê-la uniformemente (Composite). Expor um ponto único para exportação, escondendo o subsistema (Facade).

  1. Flyweight
  • TextStyle (intrínseco, imutável): font, size, color, weight.
  • StyleFactory.get(font,size,color,weight) reutiliza instâncias iguais.
  • Glyph(char, style, position) carrega estado extrínseco (posição, índice).
  1. Composite
  • Nós: Document → Section → Paragraph → GlyphRun → Glyph.
  • Operações uniformes: render(), word_count(), find(text).
  1. Facade
  • ExportFacade com to_pdf(document, path) e to_html(document, path).
  • Internamente, etapas: normalização → layout → render → persistência.

8.3 Plataforma de Notificações Multicanal (Facade + Proxy + Decorator)

Section titled “8.3 Plataforma de Notificações Multicanal (Facade + Proxy + Decorator)”

Uma empresa precisa enviar notificações por e-mail, SMS, WhatsApp e push. Há múltiplos fornecedores (gateways) por canal. Regras transversais (logging, retry/backoff, métricas) devem ser plugáveis. Latência de fornecedores varia e alguns têm limites de taxa.

Fornecer uma API única ao cliente para orquestrar envios (Facade). Interpor acesso aos adaptadores remotos com cache, rate limiting, timeouts (Proxy). Habilitar concerns opcionais (retry, logging, tracing) como camadas (Decorator).

  1. Facade
  • NotificationService.send(message, audience, channels=[…]).
  • O cliente não conhece a ordem/seleção de gateways nem retries.
  1. Proxy
  • Para cada gateway remoto (EmailGateway, SmsGateway, etc.), um GatewayProxy:
  • Timeout configurável, rate limit simples (token bucket ou janela deslizante), circuit breaker básico (abre após N falhas).
  • Cache opcional para templates/carimbos de configuração (não cachear envios).
  1. Decorator
  • RetryPolicy, Logging, Tracing decoram um Sender comum.
  • Ordem deve ser configurável (ex.: Logging(Retry(SmsSender))).