Capítulo 4 . Design Patterns

Padrões em Movimento

Cada padrão nasce de um problema.
E resolve com elegância.

Canal Sandeco
Anatomia de um padrão

Nome. Problema. Solução. Consequências.

Toda descrição de um design pattern segue essas quatro partes.

1 . Nome

Identifica o padrão em uma palavra. Cria vocabulário comum entre quem projeta, quem revisa e quem mantém.

2 . Problema

Descreve quando aplicar. Qual é a dor, em que contexto ela aparece e quais sintomas indicam que o padrão se encaixa.

3 . Solução

Os elementos que compõem o padrão: classes, objetos, suas relações e responsabilidades. O esqueleto que resolve o problema.

4 . Consequências

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.

História

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.

Notes on the Synthesis of Form The Timeless Way of Building A Pattern Language

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.

Padrão 1 de 9

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

O Problema

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.

A Solução

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ó.

Sem coesão
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.

Com Alta Coesão
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.

1

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.

2

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.

3

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.
Padrão 2 de 9

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

O Problema

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.

A Solução

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.

Sem padrão
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.

Com Baixo Acoplamento
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.

1

A classe conhece, pelo nome, várias outras classes concretas?

Construtores cheios de new ConcreteX(), imports de vendors específicos, dependências hardcoded.

2

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.

3

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.
Gang of Four

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.

Família 1

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.

Exemplos
Singleton Factory Builder Prototype
Família 2

Padrões estruturais

Compõem classes e objetos em estruturas maiores. Conectam partes incompatíveis, agregam responsabilidades e simplificam interfaces.

Exemplos
Adapter Decorator Facade Composite
Família 3

Padrões comportamentais

Definem como objetos colaboram e distribuem responsabilidades. Cuidam de algoritmos plugáveis, comunicação entre partes e fluxos de eventos.

Exemplos
Strategy Observer Command Iterator

Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides . Design Patterns: Elements of Reusable Object-Oriented Software, 1994.

Padrão 3 de 9

Singleton

Uma classe. Uma única instância. Um ponto global de acesso.

Conexão de banco

Configuração global

Logger compartilhado

O Problema

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.

A Solução

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.

Sem padrão
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.

Com Singleton
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.

1

O recurso é caro de criar?

Conexões, threads, caches, drivers. Coisas que pesam na memória ou no I/O.

2

O sistema inteiro precisa enxergar o mesmo estado?

Configurações ativas, sessão de aplicação, fila de eventos central.

3

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.
Padrão 4 de 9

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

O Problema

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.

A Solução

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.

Sem padrão
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.

Com Composite
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.

1

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.

2

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.

3

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?
Ponte entre padrões

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?

Composite

A estrutura, pronta.

Carro, Motor, Roda, Banco, Pistão, Bloco, Pneu, Aro. Todos respondem peso() na mesma interface.

Cliente faz

carro.peso()

Factory

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.

Padrão 5 de 9

Factory

Quem cria os objetos não deve ser quem os usa.

Notificações multi-canal

Gateways de pagamento

Parsers de formato

O Problema

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.

A Solução

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.

Sem padrão
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.

Com Factory
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.

1

Existem várias variantes do mesmo tipo de objeto?

Email, SMS, Push. Stripe, PayPal, Pix. Variantes da mesma família.

2

A escolha da variante depende do runtime?

Tipo vindo de config, do usuário ou do banco. Não dá para hardcodar.

3

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.
Padrão 6 de 9

Strategy

O algoritmo varia. O cliente não muda.

Cálculo de desconto

Cálculo de frete

Algoritmo de ordenação

O Problema

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.

A Solução

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.

Sem padrão
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.

Com Strategy
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.

1

Existe mais de uma forma de fazer a mesma coisa?

Cálculos de desconto, regras de frete, algoritmos de ordenação ou criptografia.

2

A escolha do algoritmo pode mudar em runtime?

Configurável por usuário, contexto, feature flag, ou tipo de cliente.

3

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.
Padrão 7 de 9

Observer

Mudou aqui. Avisou todos lá. Sem conhecer ninguém.

Eventos de domínio

Atualizações de UI

Pub/Sub interno

O Problema

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.

O Outro Problema

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.

A Solução

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.

Exemplo Real

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.

Sem padrão
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.

Com Observer
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.

1

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.

2

Os consumidores podem variar ou crescer com o tempo?

Novas reações aparecem a cada feature, algumas podem ser desligadas em runtime.

3

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.
Padrão 8 de 9

Repository

Onde os dados moram não é problema do negócio.

Persistência de entidades

Mock em testes

Banco trocável

O Problema

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.

A Soluçã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.

Sem padrão
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.

Com Repository
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.

1

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.

2

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.

3

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.
Padrão 9 de 9

Adapter

O novo conversa com o antigo. Sem reescrever nenhum dos dois.

API legada

SDK de terceiros

Sistema externo

O Problema

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.

A Soluçã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.

Sem padrão
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.

Com Adapter
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.

1

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.

2

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.

3

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.
Canal Sandeco