Nome. Problema. Solução. Consequências.
Toda descrição de um design pattern segue essas quatro partes.
Identifica o padrão em uma palavra. Cria vocabulário comum entre quem projeta, quem revisa e quem mantém.
Descreve quando aplicar. Qual é a dor, em que contexto ela aparece e quais sintomas indicam que o padrão se encaixa.
Os elementos que compõem o padrão: classes, objetos, suas relações e responsabilidades. O esqueleto que resolve o problema.
O que se ganha e o que se paga ao aplicar o padrão. Trade-offs de espaço, tempo, flexibilidade e acoplamento.
Estrutura proposta por Gamma, Helm, Johnson e Vlissides . Gang of Four, 1994.
A ideia veio da arquitetura.
O conceito de padrão nasceu antes do software como conhecemos hoje.
Arquiteto . 1936 a 2022
Christopher Alexander
Em livros publicados entre 1977 e 1979, propôs que problemas recorrentes em projetos podem ser descritos como padrões reusáveis. A ideia migrou da arquitetura para a engenharia de software décadas depois.
As seis características de um padrão, segundo Alexander
1 . Encapsulamento
Encapsula um problema ou solução bem definida. Independente, específico, claro onde se aplica.
2 . Generalidade
Permite construir outras realizações a partir dele. Não é uma solução única, é um molde.
3 . Equilíbrio
Justifica cada decisão de projeto, ponderando restrições, exemplos e soluções fracassadas.
4 . Abstração
Representa abstração da experiência empírica e do conhecimento de quem já enfrentou o problema.
5 . Abertura
Permite extensão para níveis mais baixos de detalhe. Vira ponto de partida, não de chegada.
6 . Combinatoriedade
Padrões se relacionam hierarquicamente. Alto nível compõe ou referencia o nível mais baixo.
Alta Coesão
Cada módulo, uma só razão de existir. O que mora dentro dele está conectado por um propósito.
Responsabilidade única
Métodos relacionados
Manutenção tranquila
Uma classe com mil chapéus.
Tudo cabe lá dentro: validar, mandar email, gerar PDF, salvar no banco. O nome diz uma coisa, o código faz dez.
Domínios misturados no mesmo arquivo
Quem entra na classe nunca sabe se vai mexer em auth, email, relatório ou banco. Tudo está perto demais.
Tudo num só lugar
Métodos sem nenhum parentesco entre si.
Mudança arriscada
Mexer no email pode quebrar o PDF.
Nome que mente
"UserManager" gerencia o mundo inteiro.
Uma classe, uma missão.
Cada grupo de métodos vai para uma classe focada. O nome volta a contar a verdade.
Métodos da mesma família, juntos
Valida com valida, envia email com envia email, salva com salva. Cada classe fica curta, clara e fácil de mover.
Responsabilidade única
Cada classe faz uma coisa só.
Mudança previsível
Mexer no email não toca no PDF.
Nome que entrega
UserValidator valida, e ponto.
Em código, parece com isto.
Antes, uma classe carrega assuntos diferentes. Depois, cada classe abraça um só.
class UserManager: # auth def validate_email(self, e): ... def hash_password(self, p): ... # email def send_welcome_email(self, u): ... # pdf def generate_pdf_report(self, u): ... # banco def save_to_db(self, u): ... def find_by_id(self, i): ... # util def format_date(self, d): ... # Cinco domínios sob o mesmo nome.
Auth, email, PDF, banco e utilitários moram juntos. Cada PR mexe em assuntos sem ligação entre si.
class UserValidator: def validate_email(self, e): ... def hash_password(self, p): ... class WelcomeMailer: def send(self, user): ... class UserReportPdf: def build(self, user): ... class UserRepository: def save(self, user): ... def find_by_id(self, i): ... # Cada classe abraça um assunto.
Cada classe tem nome curto, propósito claro e métodos da mesma família. Quem mexe no email não respira o PDF.
Quando aplicar Alta Coesão
Três perguntas. Se as três forem sim, divida a classe.
Os métodos da classe falam de assuntos diferentes?
Auth, email, PDF, banco e formatação convivem no mesmo arquivo, sem fio condutor entre eles.
Você precisa explicar com "e" o que ela faz?
"Gerencia usuários e manda email e gera PDF e faz log". Quando o resumo vira lista, a classe perdeu o foco.
Existem PRs que mexem só em uma parte da classe?
Bug no email, ajuste no PDF, mudança no banco. Cada um toca uma fatia isolada do arquivo, sinal claro de domínios separados.
Cuidado: coesão não é mania de classes minúsculas. Métodos que de fato falam o mesmo idioma devem ficar juntos. O alvo é foco, não fragmentação.
Alta coesão não enxuga responsabilidade. Ela junta os iguais e deixa cada classe terminar a frase que começou.
Um nome, uma missão Falta a outra face do mesmo princípio: manter os módulos com vínculos suaves.
Baixo Acoplamento
Cada módulo conhece o mínimo do mundo. Trocar um não obriga a mexer nos outros.
Dependências mínimas
Mudança isolada
Teste sem dor
Tudo conectado a tudo, com nomes próprios.
O serviço conhece cada classe concreta. Mexer numa peça pequena sacode o sistema inteiro.
Uma mudança, vários estilhaços
Trocar o banco força revisitar quem usa o banco, quem usa quem usa o banco, e segue a cascata.
Dependência direta
new MySQL(), new SendGrid(), new Stripe().
Efeito em cascata
Mexer numa peça mexe em várias.
Teste impossível
Sem como trocar dependência por fake.
Conexão por contrato, não por nome.
O serviço fala com uma abstração. Quem é a peça concreta atrás dela vira detalhe.
Dependa de abstrações
O serviço se apoia em interfaces. Trocar a implementação vira evento local.
Depende de interface
Contrato no lugar de classe concreta.
Mudança local
Troca a implementação, o resto fica intacto.
Fake em teste
Injete um stub e exercite o serviço.
Em código, parece com isto.
Antes, o serviço carrega cada vendor pelo nome. Depois, fala só com contratos.
class OrderService: def __init__(self): # Cada vendor pelo nome self.db = MySQLConnection("prod") self.mail = SendGridClient(API_KEY) self.log = FileLogger("/var/log/orders") self.pay = StripeGateway(SECRET) def place(self, order): self.db.save(order) self.pay.charge(order.total) self.mail.send(order.email) self.log.write("ok") # Trocar o banco? Mexer no construtor. # Testar sem internet? Impossível.
Quatro fornecedores fincados dentro do construtor. Cada troca pede edição, cada teste pede o mundo real.
class OrderService: def __init__(self, db, mail, log, pay): # Depende só de contratos self.db = db # Database self.mail = mail # Notifier self.log = log # Logger self.pay = pay # PaymentGateway def place(self, order): self.db.save(order) self.pay.charge(order.total) self.mail.send(order.email) self.log.write("ok") # Trocar o banco? Outra impl no main. # Testar? Passe quatro fakes.
O serviço só sabe que existe um Database, um Notifier, um Logger e um PaymentGateway. Quem entrega isso é decisão de fora.
Quando aplicar Baixo Acoplamento
Três perguntas. Se as três forem sim, use.
A classe conhece, pelo nome, várias outras classes concretas?
Construtores cheios de new ConcreteX(), imports de vendors específicos, dependências hardcoded.
Mudar uma dessas dependências obriga a tocar em quem a usa?
Trocar o banco, o gateway de pagamento ou o serviço de email dispara mudanças em cadeia pelo código.
Você quer testar essa classe sem subir o mundo real junto?
Trocar dependências por fakes ou stubs nos testes, sem precisar de banco, fila ou API externa de verdade.
Cuidado: baixo acoplamento não é zero acoplamento. Toda comunicação cria algum vínculo. A meta é que esse vínculo seja por contrato, não por implementação.
Baixo acoplamento não impede a conexão. Ele afrouxa o nó, e deixa cada módulo respirar sozinho.
Vínculos suaves, mudanças isoladas Com os dois princípios em mãos, hora de ver os clássicos que os encarnam.
Padrões GoF
Em 1994, quatro autores catalogaram 23 padrões em três famílias. Foi a obra que trouxe o conceito de Alexander para a engenharia de software.
Padrões de criação
Encapsulam o processo de instanciar objetos. Escondem detalhes da construção e dão flexibilidade ao código que pede o objeto.
Padrões estruturais
Compõem classes e objetos em estruturas maiores. Conectam partes incompatíveis, agregam responsabilidades e simplificam interfaces.
Padrões comportamentais
Definem como objetos colaboram e distribuem responsabilidades. Cuidam de algoritmos plugáveis, comunicação entre partes e fluxos de eventos.
Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides . Design Patterns: Elements of Reusable Object-Oriented Software, 1994.
Singleton
Uma classe. Uma única instância. Um ponto global de acesso.
Conexão de banco
Configuração global
Logger compartilhado
Cada serviço abrindo a própria conexão.
Cinco serviços. Dezenas de conexões. Um banco no limite.
Tempestade de conexões
Sem padrão. Cada classe instancia o que quer.
Recurso desperdiçado
Cada conexão custa memória e socket.
Pool esgota
O banco para de aceitar conexões novas.
Estado inconsistente
Cada cópia enxerga uma realidade diferente.
Uma instância. Todos pedem para ela.
Singleton centraliza o acesso. O banco respira.
getInstance() sempre devolve a mesma coisa
Uma referência. Um estado. Um ponto de controle.
Recurso preservado
Uma conexão atende todo o sistema.
Pool estável
O banco continua disponível sob carga.
Estado único
Todos enxergam a mesma verdade.
Em código, parece com isto.
A diferença mora em três linhas.
class UserService: def __init__(self): self.db = Database("prod") class OrderService: def __init__(self): self.db = Database("prod") # Cada serviço abre uma conexão nova.
Cinco serviços. Cinco conexões diferentes para o mesmo banco. Nenhuma compartilha estado.
class Database: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance UserService().db = Database() OrderService().db = Database() # Mesma instância nos dois.
A classe controla a própria criação. Qualquer chamada cai na mesma referência.
Quando aplicar Singleton
Três perguntas. Se as três forem sim, use.
O recurso é caro de criar?
Conexões, threads, caches, drivers. Coisas que pesam na memória ou no I/O.
O sistema inteiro precisa enxergar o mesmo estado?
Configurações ativas, sessão de aplicação, fila de eventos central.
Faz sentido haver só uma no processo?
Se duas instâncias quebram a lógica do domínio, é Singleton.
Cuidado: Singleton vira muleta quando vira variável global disfarçada. Use para recursos, não para conveniência.
Singleton não é sobre ter uma só. É sobre garantir que só haja uma.
A diferença é a intenção Próximo padrão: Composite.
Composite
Carro é feito de motor, rodas e bancos. Motor é feito de pistões e bloco. Você pergunta o peso ao carro, a árvore inteira responde.
Carro feito de partes
Peça e conjunto, mesma interface
Peso sobe sozinho na árvore
Cada peça vira uma referência direta.
Sem composição, o cliente segura cada objeto pelo nome: pistao_1, pneu_FE, banco_M. A soma é montada no braço, linha por linha.
Objetos espalhados, sem hierarquia
Cada peça é uma variável solta. O cliente é quem amarra tudo, somando item por item.
Referências diretas
Cliente conhece cada peça pelo nome.
Soma frágil
Esquece uma peça, total errado.
Peça nova, edita tudo
Bateria força mexer em cada cliente.
Peça e conjunto falam a mesma interface.
Pistão responde com o próprio peso. Motor responde somando os filhos. O cliente só pergunta peso() ao carro.
Mesma operação, tipos diferentes
Todos implementam Componente.peso(). A recursão mora dentro do conjunto, não no cliente.
Component único
Peça e conjunto compartilham contrato.
Recursão delegada
O conjunto soma o que os filhos retornarem.
Cliente sem if
Só chama .peso() no carro, e pronto.
Em código, parece com isto.
Antes, o cliente conhece cada peça pelo nome. Depois, o cliente conhece só a interface.
def peso_total(carro): return ( carro.motor.pistao_1.kg + carro.motor.pistao_2.kg + carro.motor.bloco.kg + carro.roda_FE.pneu.kg + carro.roda_FE.aro.kg + carro.roda_FD.pneu.kg + carro.roda_FD.aro.kg + carro.banco_M.kg + carro.banco_C.kg ) # Cliente enumera cada peça pelo nome. # Peça nova? Linha nova na soma.
Os objetos estão espalhados. O cliente é quem amarra tudo, peça por peça. Bateria nova? Edita a função em todos os lugares que somam o carro.
class Componente: def peso(self): pass class Peca(Componente): # folha def peso(self): return self.kg class Conjunto(Componente): # galho def peso(self): return sum(p.peso() for p in self.partes) # Cliente só pergunta: total = carro.peso()
A recursão mora dentro de Conjunto. Motor, Roda, Carro são conjuntos; Pistão, Pneu, Banco são peças. Tipo novo? Implementa peso(), o cliente segue intacto.
Quando aplicar Composite
Três perguntas. Se as três forem sim, use.
Existe uma hierarquia natural em árvore no domínio?
Carros e suas peças (BOM), menus e itens, departamentos e funcionários, componentes de UI dentro de outros componentes.
O cliente quer tratar parte e todo da mesma forma?
A mesma operação faz sentido na peça e no conjunto: pesar, calcular custo, render, exportar. Diferenciar só estorva.
Você precisa adicionar tipos de nó sem mexer no cliente?
Hoje tem Motor e Roda. Amanhã chega Bateria ou Suspensão. O cliente não pode virar uma sequência de isinstance.
Cuidado: Composite força peça e conjunto a compartilharem interface. Se a operação só faz sentido em um deles, melhor manter dois tipos separados que inchar o contrato.
Você pergunta o peso do carro. A árvore inteira responde, no mesmo tom, até o último parafuso.
Peça e conjunto, mesma interface Mas quem monta essa árvore toda?
Composite descreve. Factory monta.
Você tem a árvore do carro com peças e conjuntos. Agora a pergunta vira: quem instancia tudo isso, na ordem certa, sem espalhar new pelo cliente?
A estrutura, pronta.
Carro, Motor, Roda, Banco, Pistão, Bloco, Pneu, Aro. Todos respondem peso() na mesma interface.
Cliente faz
carro.peso()
quem
monta?
A construção, escondida.
CarroFactory.montar("sedan") devolve a árvore inteira já montada. O cliente nem sabe a ordem.
Cliente faz
carro = Factory.montar("sedan")
Os dois padrões se completam: Composite define o que o objeto é. Factory define como ele nasce. Composite sem Factory força o cliente a montar a árvore na mão; Factory sem Composite devolve só um objeto raso.
Factory
Quem cria os objetos não deve ser quem os usa.
Notificações multi-canal
Gateways de pagamento
Parsers de formato
Cada cliente conhece todos os tipos.
Tipo novo significa tocar em todos os clientes.
Acoplamento direto às concretas
O if/elif sobre tipo se repete em vários arquivos.
Conhecimento espalhado
Cada cliente importa todas as concretas.
if/elif duplicado
A mesma decisão aparece em vários lugares.
Mudança caça-fantasma
Tipo novo, vários arquivos para editar.
Uma fábrica decide por todos.
Clientes pedem pela interface. A Factory escolhe a concreta.
create() encapsula a escolha do tipo
Um único ponto sabe construir cada concreta.
Cliente despreocupado
Pede pela interface. Usa o objeto pronto.
Concretas escondidas
Detalhes ficam dentro da Factory.
Extensão pacífica
Tipo novo, só a Factory muda.
Em código, parece com isto.
Antes, o if/elif vive no cliente. Depois, mora na fábrica.
class CheckoutService: def notify(self, kind, msg): if kind == "email": n = EmailNotification() elif kind == "sms": n = SmsNotification() elif kind == "push": n = PushNotification() n.send(msg) # O mesmo if/elif aparece em vários serviços.
Cada serviço carrega o mesmo bloco de decisão. Adicionar WhatsApp obriga editar todos.
class NotificationFactory: @staticmethod def create(kind): return { "email": EmailNotification, "sms": SmsNotification, "push": PushNotification, }[kind]() NotificationFactory.create("email").send(msg) # Tipo novo? Só edita a Factory.
O cliente pede pelo nome do tipo. A fábrica entrega a concreta correta.
Quando aplicar Factory
Três perguntas. Se as três forem sim, use.
Existem várias variantes do mesmo tipo de objeto?
Email, SMS, Push. Stripe, PayPal, Pix. Variantes da mesma família.
A escolha da variante depende do runtime?
Tipo vindo de config, do usuário ou do banco. Não dá para hardcodar.
O conjunto de variantes provavelmente vai crescer?
Adicionar uma variante nova deve ser barato. Edita um arquivo, não cinco.
Cuidado: Factory só ganha sentido quando o cliente não deveria saber a concreta. Se só existe um tipo possível, é overengineering.
Factory não cria objetos. Factory esconde como eles são criados.
O cliente fica de fora da decisão Próximo padrão: Strategy.
Strategy
O algoritmo varia. O cliente não muda.
Cálculo de desconto
Cálculo de frete
Algoritmo de ordenação
Um método que cresce a cada regra.
if/elif gigante. Cada promoção engorda a mesma classe.
Algoritmos hardcoded na classe
Toda regra mora dentro do mesmo método. Trocar significa editar a classe.
Classe inchada
calculate() vira um mural de promoções.
Regras misturadas
Mexer em uma ameaça as outras.
Difícil de testar
Tudo precisa rodar junto, sempre.
Cada algoritmo numa classe. Trocáveis.
O cliente delega. A estratégia decide como.
strategy.apply() encapsula a regra
Cart tem um slot. A estratégia escolhida resolve.
Algoritmos isolados
Cada regra numa classe própria.
Trocáveis em runtime
Política muda sem recompilar.
Cart inalterado
Regra nova, classe nova. Cart segue igual.
Em código, parece com isto.
Antes, o if/elif vive na classe. Depois, cada caso tem sua classe.
class ShoppingCart: def calculate(self, kind, price): if kind == "regular": return price elif kind == "vip": return price * 0.90 elif kind == "blackFriday": return price * 0.75 elif kind == "employee": return price * 0.65 # O método cresce a cada promo.
Toda regra mora dentro do mesmo método. Adicionar Aniversariante obriga editar a classe.
class PricingStrategy: def apply(self, price): pass class VipPricing(PricingStrategy): def apply(self, price): return price * 0.90 class ShoppingCart: def __init__(self, strategy): self.strategy = strategy def calculate(self, price): return self.strategy.apply(price) # Regra nova? Nova classe. Cart intacto.
A classe pede a uma interface. Quem decide como calcular é a estratégia injetada.
Quando aplicar Strategy
Três perguntas. Se as três forem sim, use.
Existe mais de uma forma de fazer a mesma coisa?
Cálculos de desconto, regras de frete, algoritmos de ordenação ou criptografia.
A escolha do algoritmo pode mudar em runtime?
Configurável por usuário, contexto, feature flag, ou tipo de cliente.
Adicionar uma forma nova é provável?
Promo nova a cada trimestre, regra inventada por marketing, algoritmo experimental.
Cuidado: se só existe um algoritmo possível e ele nunca muda, Strategy vira indireção desnecessária. Use só quando o ponto de variação for real.
Strategy não é escolher o algoritmo. É poder trocá-lo sem reescrever quem usa.
O comportamento é plugável Próximo padrão: Observer.
Observer
Mudou aqui. Avisou todos lá. Sem conhecer ninguém.
Eventos de domínio
Atualizações de UI
Pub/Sub interno
Cada nova reação obriga editar o objeto.
Order chama Email, Invoice, Stock, Analytics. E mais um a cada feature.
Subject acoplado a cada consumidor
Order importa e chama cada serviço. Adicionar reação significa editar Order.
Subject inchado
confirm() vira lista de chamadas.
Acoplamento direto
Order conhece cada serviço por nome.
Mudar é arriscado
Toca em Order, quebra alguém downstream.
Sem aviso, todos perguntam o tempo todo.
Polling é o oposto de Observer: gira em while gastando CPU à toa.
Subject passivo, consumidores ansiosos
Sem mecanismo de notificação, cada consumidor pergunta em loop infinito.
Polling sem fim
Cada consumer gira em while perguntando.
CPU desperdiçada
A maioria das consultas volta vazia.
Latência ruim
Aviso só chega no próximo ciclo de poll.
Quem se interessa, se inscreve.
Order só publica. Os observers cuidam de si.
subscribe() e notify() invertem a dependência
Observers se registram. Subject emite. Ninguém é nomeado no código do Subject.
Subject só publica
Não conhece quem reage ao evento.
Observers plugáveis
Subscribe e unsubscribe em runtime.
Cresce sem dor
Reação nova, observer novo. Subject intacto.
WhatsApp é Observer em escala.
Seu celular não pergunta se chegou. O servidor avisa no momento exato.
Server publica, celulares só escutam
Cada aparelho mantém uma conexão. O server empurra quando há novidade.
Bateria preservada
Celular não fica perguntando o tempo todo.
Entrega imediata
O aviso chega no exato instante em que existe.
Escala global
Bilhões de aparelhos no mesmo padrão.
Em código, parece com isto.
Antes, Order liga para cada um. Depois, Order só publica.
class Order: def confirm(self): # ... lógica de negócio EmailService.send(self) InvoiceGenerator.create(self) StockManager.reserve(self) Analytics.track(self) AuditLog.write(self) # Reação nova? Edita Order de novo.
Order conhece todo mundo pelo nome. Cada feature nova obriga a abrir essa classe e mexer.
class Order: def __init__(self): self._subs = [] def subscribe(self, obs): self._subs.append(obs) def confirm(self): # ... lógica de negócio for obs in self._subs: obs.update(self, "confirmed") # Reação nova? Novo observer. Order intacto.
Order só sabe que tem uma lista de algo que responde a update(). Quem se inscreve resolve seu próprio destino.
Quando aplicar Observer
Três perguntas. Se as três forem sim, use.
Uma mudança em A deve disparar reações em vários lugares?
Eventos de domínio, ações com efeitos colaterais múltiplos, atualizações de UI.
Os consumidores podem variar ou crescer com o tempo?
Novas reações aparecem a cada feature, algumas podem ser desligadas em runtime.
O publicador não deveria conhecer cada consumidor?
Inverter a dependência reduz acoplamento e isola o motivo da mudança.
Cuidado: Observer cria fluxos invisíveis. Em sistemas pequenos, chamadas explícitas são mais legíveis. Só use quando a indireção paga seu preço.
Observer não notifica. Observer permite que outros escutem.
A dependência é invertida Próximo padrão: Repository.
Repository
Onde os dados moram não é problema do negócio.
Persistência de entidades
Mock em testes
Banco trocável
SQL espalhado por todo o sistema.
Cada serviço escreve sua própria query. Trocar o banco quebra metade do código.
Negócio acoplado ao banco
Serviços conhecem tabelas, colunas e dialeto SQL. Mudar de Postgres para Mongo refaz tudo.
SQL exposto
Query crua dentro da lógica de negócio.
Teste difícil
Cada teste precisa subir banco real.
Troca custa caro
Mudou o banco, mudou todo serviço.
Uma fronteira entre negócio e dados.
O serviço pede dados. O repositório decide onde buscar.
Interface esconde a infraestrutura
Postgres, Mongo ou InMemory ficam atrás do mesmo contrato. O serviço não distingue.
Domínio limpo
Serviço só conhece o repositório.
Teste com mock
InMemoryRepo nos testes, sem subir banco.
Banco plugável
Troca de implementação, domínio intacto.
Em código, parece com isto.
Antes, o serviço escreve SQL. Depois, o serviço pede ao repositório.
class UserService: def __init__(self, conn): self.conn = conn def activate(self, user_id): cur = self.conn.cursor() cur.execute( "SELECT * FROM users WHERE id = %s", (user_id,) ) row = cur.fetchone() # ... regra de negócio cur.execute( "UPDATE users SET active=1 WHERE id=%s", (user_id,) ) # SQL e dialeto do banco dentro do domínio.
A regra de negócio convive com cursor, tabela e sintaxe SQL. Trocar o banco quebra a classe inteira.
class UserRepository: def find_by_id(self, id): pass def save(self, user): pass class UserService: def __init__(self, repo): self.repo = repo def activate(self, user_id): user = self.repo.find_by_id(user_id) user.activate() self.repo.save(user) # Banco novo? Repo novo. Service intacto.
O serviço fala a linguagem do domínio. Postgres, Mongo ou InMemory ficam atrás da mesma interface.
Quando aplicar Repository
Três perguntas. Se as três forem sim, use.
Existe lógica de negócio que mexe com dados persistentes?
Entidades que vivem em banco, cache ou arquivo, e regras que operam sobre elas.
Você quer testar a lógica sem subir banco real?
Repositório fake em memória deixa os testes rápidos, isolados e determinísticos.
A camada de persistência pode mudar com o tempo?
ORM novo, banco diferente, cache na frente, leitura em réplica. Tudo isso fica atrás da interface.
Cuidado: em scripts curtos ou CRUDs minúsculos, Repository vira indireção sem retorno. Use quando o domínio for grande o bastante para merecer uma fronteira.
Repository não é classe de query. É a fronteira que protege o domínio das mudanças de infraestrutura.
O domínio fica imune ao banco Próximo padrão: Adapter.
Adapter
O novo conversa com o antigo. Sem reescrever nenhum dos dois.
API legada
SDK de terceiros
Sistema externo
Cliente novo, sistema antigo, interfaces incompatíveis.
Métodos diferentes, formatos diferentes. O cliente não consegue chamar direto.
Chamadas batem na parede
O cliente fala uma interface, o legado fala outra. Sem ponte, nada flui.
Métodos não batem
charge() não existe no legado.
Formato estranho
XML cru entra, XML cru volta.
Legado intocável
Mudar a fonte não é opção.
Um tradutor no meio do caminho.
Adapter fala a língua do cliente e a língua do legado. Os dois ficam intactos.
Adapter implementa a interface esperada
Por dentro, ele converte e delega para o sistema antigo. Nem o cliente nem o legado precisam mudar.
Cliente intacto
Ele só conhece a interface limpa.
Legado intacto
Continua expondo o que sempre expôs.
Adapter isolado
A tradução mora num único lugar.
Em código, parece com isto.
Antes, o cliente carrega a complexidade do legado. Depois, o adapter assume.
class ModernCheckout: def pay(self, amount): # Tem que falar a língua do legado xml = LegacyGateway.executeTransaction({ "type": "PAY", "amt": amount, "fmt": "XML" }) # E parsear XML cru na volta result = XMLParser.parse(xml) return result.find("status").text # Negócio misturado com tradução.
A regra de pagamento convive com formato XML, dialeto legado e parsing. Cada novo cliente repete tudo.
class PaymentGateway: def charge(self, amount): pass class LegacyAdapter(PaymentGateway): def __init__(self, legacy): self.legacy = legacy def charge(self, amount): xml = self.legacy.executeTransaction({ "type": "PAY", "amt": amount }) return self._parse(xml) class ModernCheckout: def pay(self, gateway, amount): return gateway.charge(amount) # Tradução mora dentro do adapter.
O cliente fala a interface limpa. O adapter assume o trabalho de converter, parsear e delegar.
Quando aplicar Adapter
Três perguntas. Se as três forem sim, use.
Existe um sistema externo ou legado que você não pode alterar?
SDK de terceiros, biblioteca proprietária, serviço corporativo congelado, código antigo sem testes.
A interface dele é diferente da que o seu código espera?
Nomes de método, tipos, formatos, protocolos: tudo desencontrado da interface do domínio.
Você quer isolar o resto do código dessa incompatibilidade?
Concentrar a tradução num único ponto, evitando que ela contamine cada chamador.
Cuidado: se você controla os dois lados e pode evoluir um deles, refatorar costuma ser mais limpo que adicionar um adapter. Use quando a fronteira for inegociável.
Adapter não muda o velho nem força o novo. Ele traduz, e deixa cada lado falar sua própria língua.
Duas interfaces, uma ponte Os nove padrões, completos.