Python

Dominando Design Patterns em Python: Factory, Repository, Observer e Strategy em Projetos Reais

18 min de leitura

Dominando Design Patterns em Python: Factory, Repository, Observer e Strategy em Projetos Reais

Design Patterns em Python: Factory, Repository, Observer e Strategy Design patterns são soluções comprovadas para problemas recorrentes no desenvolvimento de software. Eles não são códigos prontos para copiar, mas sim templates de como estruturar seu código para resolver desafios específicos de forma elegante e manutenível. Neste artigo, exploraremos quatro padrões essenciais que todo desenvolvedor Python deve compreender: Factory (criação de objetos), Repository (acesso a dados), Observer (comunicação entre objetos) e Strategy (algoritmos intercambiáveis). A importância desses padrões vai além da teoria acadêmica. Quando você trabalha em projetos reais, projetos grandes com múltiplos desenvolvedores, eventualmente terá que lidar com código complexo que precisa ser fácil de testar, estender e manter. Design patterns fornecem um vocabulário comum que permite comunicação clara entre desenvolvedores e reduz a necessidade de explicações detalhadas sobre a arquitetura do código. Factory Pattern: Criando Objetos de Forma Inteligente O Conceito Fundamental O padrão Factory resolve um problema simples mas fundamental: como criar objetos sem acoplamento direto à classe

<h2>Design Patterns em Python: Factory, Repository, Observer e Strategy</h2>

<p>Design patterns são soluções comprovadas para problemas recorrentes no desenvolvimento de software. Eles não são códigos prontos para copiar, mas sim templates de como estruturar seu código para resolver desafios específicos de forma elegante e manutenível. Neste artigo, exploraremos quatro padrões essenciais que todo desenvolvedor Python deve compreender: Factory (criação de objetos), Repository (acesso a dados), Observer (comunicação entre objetos) e Strategy (algoritmos intercambiáveis).</p>

<p>A importância desses padrões vai além da teoria acadêmica. Quando você trabalha em projetos reais, projetos grandes com múltiplos desenvolvedores, eventualmente terá que lidar com código complexo que precisa ser fácil de testar, estender e manter. Design patterns fornecem um vocabulário comum que permite comunicação clara entre desenvolvedores e reduz a necessidade de explicações detalhadas sobre a arquitetura do código.</p>

<h2>Factory Pattern: Criando Objetos de Forma Inteligente</h2>

<h3>O Conceito Fundamental</h3>

<p>O padrão Factory resolve um problema simples mas fundamental: como criar objetos sem acoplamento direto à classe concreta? Em vez de usar <code>new</code> ou o construtor diretamente em diversos lugares do código, você delega a criação para uma classe ou método especializado. Isso permite que você mude a implementação sem alterar o código que utiliza o objeto.</p>

<p>Imagine um sistema que trabalha com diferentes tipos de pagamento: cartão de crédito, boleto, PIX. Se você espalhar <code>if/elif</code> para instanciar cada um por todo o código, uma simples mudança se torna um pesadelo. Factory centraliza isso em um único lugar.</p>

<h3>Implementação Prática com Factory Method</h3>

<pre><code class="language-python">from abc import ABC, abstractmethod

Classes abstratas que definem o contrato

class Pagamento(ABC):

@abstractmethod

def processar(self, valor: float) -&gt; bool:

pass

class CartaoCredito(Pagamento):

def processar(self, valor: float) -&gt; bool:

print(f&quot;Processando R$ {valor} no cartão de crédito&quot;)

return True

class Boleto(Pagamento):

def processar(self, valor: float) -&gt; bool:

print(f&quot;Gerando boleto de R$ {valor}&quot;)

return True

class PIX(Pagamento):

def processar(self, valor: float) -&gt; bool:

print(f&quot;Enviando PIX de R$ {valor}&quot;)

return True

Factory Method

class PagamentoFactory:

_tipos = {

&#039;cartao&#039;: CartaoCredito,

&#039;boleto&#039;: Boleto,

&#039;pix&#039;: PIX

}

@staticmethod

def criar(tipo: str) -&gt; Pagamento:

if tipo not in PagamentoFactory._tipos:

raise ValueError(f&quot;Tipo de pagamento &#039;{tipo}&#039; não suportado&quot;)

return PagamentoFactory._tipos[tipo]()

Uso

pagamento = PagamentoFactory.criar(&#039;pix&#039;)

pagamento.processar(150.00)

pagamento = PagamentoFactory.criar(&#039;cartao&#039;)

pagamento.processar(500.00)</code></pre>

<p>Observe que o código cliente não conhece as classes concretas. Se você precisar adicionar um novo método de pagamento (como Google Pay), basta criar a classe e registrá-la no dicionário <code>_tipos</code>. Não há necessidade de alterar o código que usa a factory.</p>

<h3>Variação com Abstract Factory</h3>

<p>Para cenários mais complexos onde você precisa criar famílias de objetos relacionados, use Abstract Factory:</p>

<pre><code class="language-python">from abc import ABC, abstractmethod

class BotaoUI(ABC):

@abstractmethod

def renderizar(self) -&gt; str:

pass

class CampoUI(ABC):

@abstractmethod

def renderizar(self) -&gt; str:

pass

class BotaoWindows(BotaoUI):

def renderizar(self) -&gt; str:

return &quot;Botão com estilo Windows&quot;

class BotaoMac(BotaoUI):

def renderizar(self) -&gt; str:

return &quot;Botão com estilo Mac&quot;

class CampoWindows(CampoUI):

def renderizar(self) -&gt; str:

return &quot;Campo com aparência Windows&quot;

class CampoMac(CampoUI):

def renderizar(self) -&gt; str:

return &quot;Campo com aparência Mac&quot;

class FactoryUI(ABC):

@abstractmethod

def criar_botao(self) -&gt; BotaoUI:

pass

@abstractmethod

def criar_campo(self) -&gt; CampoUI:

pass

class FactoryWindows(FactoryUI):

def criar_botao(self) -&gt; BotaoUI:

return BotaoWindows()

def criar_campo(self) -&gt; CampoUI:

return CampoWindows()

class FactoryMac(FactoryUI):

def criar_botao(self) -&gt; BotaoUI:

return BotaoMac()

def criar_campo(self) -&gt; CampoUI:

return CampoMac()

Uso

def criar_interface(sistema_operacional: str):

if sistema_operacional == &#039;windows&#039;:

factory = FactoryWindows()

else:

factory = FactoryMac()

botao = factory.criar_botao()

campo = factory.criar_campo()

print(botao.renderizar())

print(campo.renderizar())

criar_interface(&#039;windows&#039;)</code></pre>

<p>A Abstract Factory garante que você sempre cria componentes relacionados (nunca mistura um botão Windows com um campo Mac).</p>

<h2>Repository Pattern: Abstraindo o Acesso a Dados</h2>

<h3>Por Que Abstrair Dados?</h3>

<p>O padrão Repository funciona como um intermediário entre a lógica de negócio e a camada de dados. Em vez de espalhar queries SQL ou chamadas a bancos de dados por todo seu código, você centraliza isso. O grande benefício? Você consegue trocar de banco de dados ou implementação sem mexer na lógica de negócio. Também fica trivial fazer testes sem um banco de dados real.</p>

<h3>Repository Genérico com Python</h3>

<pre><code class="language-python">from abc import ABC, abstractmethod

from typing import List, Optional, TypeVar, Generic

import json

import os

T = TypeVar(&#039;T&#039;)

Definição abstrata do repositório

class Repository(ABC, Generic[T]):

@abstractmethod

def adicionar(self, entidade: T) -&gt; None:

pass

@abstractmethod

def obter_por_id(self, id: int) -&gt; Optional[T]:

pass

@abstractmethod

def obter_todos(self) -&gt; List[T]:

pass

@abstractmethod

def atualizar(self, entidade: T) -&gt; None:

pass

@abstractmethod

def deletar(self, id: int) -&gt; None:

pass

Entidade de domínio

class Usuario:

def __init__(self, id: int, nome: str, email: str):

self.id = id

self.nome = nome

self.email = email

def __repr__(self):

return f&quot;Usuario(id={self.id}, nome=&#039;{self.nome}&#039;, email=&#039;{self.email}&#039;)&quot;

Implementação em memória (perfeita para testes)

class RepositorioUsuarioEmMemoria(Repository[Usuario]):

def __init__(self):

self._usuarios = {}

def adicionar(self, usuario: Usuario) -&gt; None:

self._usuarios[usuario.id] = usuario

def obter_por_id(self, id: int) -&gt; Optional[Usuario]:

return self._usuarios.get(id)

def obter_todos(self) -&gt; List[Usuario]:

return list(self._usuarios.values())

def atualizar(self, usuario: Usuario) -&gt; None:

if usuario.id in self._usuarios:

self._usuarios[usuario.id] = usuario

def deletar(self, id: int) -&gt; None:

if id in self._usuarios:

del self._usuarios[id]

Implementação em JSON (simulando persistência)

class RepositorioUsuarioJSON(Repository[Usuario]):

def __init__(self, arquivo: str = &quot;usuarios.json&quot;):

self.arquivo = arquivo

self._garantir_arquivo()

def _garantir_arquivo(self):

if not os.path.exists(self.arquivo):

with open(self.arquivo, &#039;w&#039;) as f:

json.dump({}, f)

def _carregar(self) -&gt; dict:

with open(self.arquivo, &#039;r&#039;) as f:

return json.load(f)

def _salvar(self, dados: dict):

with open(self.arquivo, &#039;w&#039;) as f:

json.dump(dados, f, indent=2)

def adicionar(self, usuario: Usuario) -&gt; None:

dados = self._carregar()

dados[str(usuario.id)] = {

&#039;id&#039;: usuario.id,

&#039;nome&#039;: usuario.nome,

&#039;email&#039;: usuario.email

}

self._salvar(dados)

def obter_por_id(self, id: int) -&gt; Optional[Usuario]:

dados = self._carregar()

if str(id) in dados:

u = dados[str(id)]

return Usuario(u[&#039;id&#039;], u[&#039;nome&#039;], u[&#039;email&#039;])

return None

def obter_todos(self) -&gt; List[Usuario]:

dados = self._carregar()

return [Usuario(u[&#039;id&#039;], u[&#039;nome&#039;], u[&#039;email&#039;]) for u in dados.values()]

def atualizar(self, usuario: Usuario) -&gt; None:

dados = self._carregar()

dados[str(usuario.id)] = {

&#039;id&#039;: usuario.id,

&#039;nome&#039;: usuario.nome,

&#039;email&#039;: usuario.email

}

self._salvar(dados)

def deletar(self, id: int) -&gt; None:

dados = self._carregar()

if str(id) in dados:

del dados[str(id)]

self._salvar(dados)

Serviço de negócio que usa o repositório

class ServicoUsuario:

def __init__(self, repositorio: Repository[Usuario]):

self.repositorio = repositorio

def registrar_usuario(self, id: int, nome: str, email: str) -&gt; Usuario:

usuario = Usuario(id, nome, email)

self.repositorio.adicionar(usuario)

return usuario

def obter_usuario(self, id: int) -&gt; Optional[Usuario]:

return self.repositorio.obter_por_id(id)

def listar_usuarios(self) -&gt; List[Usuario]:

return self.repositorio.obter_todos()

Uso

if __name__ == &quot;__main__&quot;:

Teste com repositório em memória

repo_memoria = RepositorioUsuarioEmMemoria()

servico = ServicoUsuario(repo_memoria)

servico.registrar_usuario(1, &quot;João Silva&quot;, &quot;joao@example.com&quot;)

servico.registrar_usuario(2, &quot;Maria Santos&quot;, &quot;maria@example.com&quot;)

print(&quot;Usuários em memória:&quot;, servico.listar_usuarios())

Se precisar trocar para JSON, apenas uma linha muda:

repo_json = RepositorioUsuarioJSON()

servico = ServicoUsuario(repo_json)

servico.registrar_usuario(3, &quot;Pedro Costa&quot;, &quot;pedro@example.com&quot;)

print(&quot;Usuários em JSON:&quot;, servico.listar_usuarios())</code></pre>

<p>Veja como a classe <code>ServicoUsuario</code> não precisa saber se os dados vêm de um banco de dados real, JSON ou até uma API. Ela trabalha com a abstração <code>Repository</code>, permitindo que você mude a implementação conforme necessário, inclusive para testes.</p>

<h2>Observer Pattern: Comunicação Reativa Entre Objetos</h2>

<h3>Entendendo o Fluxo de Eventos</h3>

<p>O padrão Observer é perfeito para cenários onde múltiplos objetos precisam reagir a mudanças em outro objeto. Em vez de polling (perguntar constantemente &quot;mudou?&quot;), o Observer implementa push: quando algo muda, notifica interessados automaticamente. É amplamente usado em interfaces gráficas, sistemas de eventos e arquiteturas reativas.</p>

<h3>Implementação Clássica</h3>

<pre><code class="language-python">from abc import ABC, abstractmethod

from typing import List

Interface para observadores

class Observador(ABC):

@abstractmethod

def atualizar(self, evento: str, dados: dict) -&gt; None:

pass

Sujeito (observable)

class Conta:

def __init__(self, saldo_inicial: float = 0):

self._saldo = saldo_inicial

self._observadores: List[Observador] = []

def anexar(self, observador: Observador) -&gt; None:

if observador not in self._observadores:

self._observadores.append(observador)

def desanexar(self, observador: Observador) -&gt; None:

if observador in self._observadores:

self._observadores.remove(observador)

def _notificar(self, evento: str, dados: dict) -&gt; None:

for observador in self._observadores:

observador.atualizar(evento, dados)

def depositar(self, valor: float) -&gt; None:

self._saldo += valor

self._notificar(&#039;deposito&#039;, {

&#039;valor&#039;: valor,

&#039;saldo_novo&#039;: self._saldo

})

def sacar(self, valor: float) -&gt; None:

if valor &lt;= self._saldo:

self._saldo -= valor

self._notificar(&#039;saque&#039;, {

&#039;valor&#039;: valor,

&#039;saldo_novo&#039;: self._saldo

})

else:

self._notificar(&#039;saque_rejeitado&#039;, {

&#039;valor&#039;: valor,

&#039;saldo_disponivel&#039;: self._saldo

})

@property

def saldo(self) -&gt; float:

return self._saldo

Observadores concretos

class LogTransacao(Observador):

def atualizar(self, evento: str, dados: dict) -&gt; None:

print(f&quot;[LOG] Evento: {evento} | Dados: {dados}&quot;)

class NotificacaoEmail(Observador):

def atualizar(self, evento: str, dados: dict) -&gt; None:

if evento in [&#039;deposito&#039;, &#039;saque&#039;]:

print(f&quot;[EMAIL] Transação realizada: {evento}&quot;)

print(f&quot;[EMAIL] Novo saldo: R$ {dados.get(&#039;saldo_novo&#039;)}&quot;)

class AlertaSaldoBaixo(Observador):

def __init__(self, limite: float = 100):

self.limite = limite

def atualizar(self, evento: str, dados: dict) -&gt; None:

saldo = dados.get(&#039;saldo_novo&#039;)

if saldo is not None and saldo &lt; self.limite:

print(f&quot;[ALERTA] Saldo baixo! R$ {saldo} &lt; R$ {self.limite}&quot;)

Uso

conta = Conta(1000)

log = LogTransacao()

email = NotificacaoEmail()

alerta = AlertaSaldoBaixo(200)

conta.anexar(log)

conta.anexar(email)

conta.anexar(alerta)

print(&quot;=== Depósito ===&quot;)

conta.depositar(500)

print(&quot;\n=== Saque pequeno ===&quot;)

conta.sacar(100)

print(&quot;\n=== Saque grande ===&quot;)

conta.sacar(1200)

print(f&quot;\nSaldo final: R$ {conta.saldo}&quot;)</code></pre>

<p>Note que adicionar novos observadores não afeta a classe <code>Conta</code>. Se você precisar de um SMS de alerta ou sincronizar com um sistema externo, basta criar um novo observador. Essa desacoplagem é o poder real do padrão.</p>

<h3>Observer com Decoradores (Pythônico)</h3>

<p>Python permite uma abordagem mais elegante usando decoradores:</p>

<pre><code class="language-python">from functools import wraps

from typing import Callable, List, Dict, Any

class EventManager:

def __init__(self):

self._listeners: Dict[str, List[Callable]] = {}

def on(self, evento: str):

&quot;&quot;&quot;Decorador para registrar handlers de eventos&quot;&quot;&quot;

def decorador(func: Callable) -&gt; Callable:

if evento not in self._listeners:

self._listeners[evento] = []

self._listeners[evento].append(func)

return func

return decorador

def emit(self, evento: str, **dados: Any) -&gt; None:

&quot;&quot;&quot;Dispara um evento para todos os listeners&quot;&quot;&quot;

if evento in self._listeners:

for handler in self._listeners[evento]:

handler(**dados)

Uso com decoradores

gerenciador = EventManager()

@gerenciador.on(&#039;usuario_criado&#039;)

def enviar_email_boas_vindas(usuario_id: int, email: str, **kwargs):

print(f&quot;[EMAIL] Bem-vindo {email}!&quot;)

@gerenciador.on(&#039;usuario_criado&#039;)

def registrar_audit(usuario_id: int, **kwargs):

print(f&quot;[AUDIT] Novo usuário criado: {usuario_id}&quot;)

Dispara o evento

gerenciador.emit(&#039;usuario_criado&#039;, usuario_id=1, email=&#039;novo@example.com&#039;)</code></pre>

<h2>Strategy Pattern: Intercambiando Algoritmos em Tempo de Execução</h2>

<h3>O Problema de Múltiplos Caminhos</h3>

<p>Frequentemente, você precisa executar diferentes algoritmos baseado em condições. A abordagem ingênua é encher seu código com <code>if/elif/else</code>. O Strategy pattern diz: encapsule cada algoritmo em uma classe separada e deixe o cliente escolher qual usar. Quando você precisa de um novo algoritmo, não modifica o existente; você apenas adiciona um novo.</p>

<h3>Implementação Prática com Cálculo de Preços</h3>

<pre><code class="language-python">from abc import ABC, abstractmethod

from datetime import datetime

Interface para estratégias de desconto

class EstrategiaDesconto(ABC):

@abstractmethod

def calcular_desconto(self, valor: float) -&gt; float:

pass

class SemDesconto(EstrategiaDesconto):

def calcular_desconto(self, valor: float) -&gt; float:

return 0

class DescontoFixo(EstrategiaDesconto):

def __init__(self, percentual: float):

self.percentual = percentual

def calcular_desconto(self, valor: float) -&gt; float:

return valor * (self.percentual / 100)

class DescontoProgressivo(EstrategiaDesconto):

&quot;&quot;&quot;Quanto maior a compra, maior o desconto&quot;&quot;&quot;

def calcular_desconto(self, valor: float) -&gt; float:

if valor &gt; 1000:

return valor * 0.20

elif valor &gt; 500:

return valor * 0.10

elif valor &gt; 100:

return valor * 0.05

return 0

class DescontoParaCliente(EstrategiaDesconto):

&quot;&quot;&quot;Black Friday ou oferta especial&quot;&quot;&quot;

def calcular_desconto(self, valor: float) -&gt; float:

Simula ofertas em horários específicos

hora_atual = datetime.now().hour

if 20 &lt;= hora_atual &lt;= 23: # Oferta noturna

return valor * 0.15

return 0

Contexto que usa a estratégia

class Pedido:

def __init__(self, items: list, estrategia_desconto: EstrategiaDesconto = None):

self.items = items

self.estrategia = estrategia_desconto or SemDesconto()

def mudar_estrategia(self, estrategia: EstrategiaDesconto) -&gt; None:

self.estrategia = estrategia

def calcular_total(self) -&gt; dict:

subtotal = sum(item[&#039;preco&#039;] * item[&#039;quantidade&#039;] for item in self.items)

desconto = self.estrategia.calcular_desconto(subtotal)

total = subtotal - desconto

return {

&#039;subtotal&#039;: subtotal,

&#039;desconto&#039;: desconto,

&#039;total&#039;: total,

&#039;estrategia&#039;: self.estrategia.__class__.__name__

}

Uso

items = [

{&#039;preco&#039;: 100, &#039;quantidade&#039;: 2},

{&#039;preco&#039;: 150, &#039;quantidade&#039;: 3}

]

Cliente normal, sem desconto

pedido = Pedido(items)

print(&quot;Sem desconto:&quot;, pedido.calcular_total())

Cliente especial com desconto fixo

pedido.mudar_estrategia(DescontoFixo(10))

print(&quot;Com 10% fixo:&quot;, pedido.calcular_total())

Cliente que fez uma grande compra

pedido.mudar_estrategia(DescontoProgressivo())

print(&quot;Com desconto progressivo:&quot;, pedido.calcular_total())

Black Friday

pedido.mudar_estrategia(DescontoParaCliente())

print(&quot;Black Friday:&quot;, pedido.calcular_total())</code></pre>

<h3>Strategy com Funções (Mais Simples)</h3>

<p>Para casos mais simples, Python permite usar funções diretamente como estratégias:</p>

<pre><code class="language-python">from typing import Callable

class ProcessadorPagamento:

def __init__(self, estrategia_taxa: Callable[[float], float]):

self.estrategia_taxa = estrategia_taxa

def processar(self, valor: float) -&gt; dict:

taxa = self.estrategia_taxa(valor)

total = valor + taxa

return {

&#039;valor_original&#039;: valor,

&#039;taxa&#039;: taxa,

&#039;total_cobrado&#039;: total

}

Estratégias como funções simples

def taxa_cartao_credito(valor: float) -&gt; float:

return valor * 0.03

def taxa_boleto(valor: float) -&gt; float:

return 3.50 # Taxa fixa

def taxa_pix(valor: float) -&gt; float:

return 0 # PIX sem taxa

Uso

processador = ProcessadorPagamento(taxa_pix)

print(processador.processar(100))

processador = ProcessadorPagamento(taxa_cartao_credito)

print(processador.processar(100))

processador = ProcessadorPagamento(taxa_boleto)

print(processador.processar(100))</code></pre>

<p>Essa abordagem funcional é perfeita quando suas estratégias são simples. Para lógica mais complexa, use classes.</p>

<h2>Conclusão</h2>

<p>Dominar esses quatro padrões transforma você em um desenvolvedor que escreve código profissional e escalável. <strong>Factory</strong> elimina o acoplamento à criação de objetos, permitindo que você adicione novos tipos sem modificar código existente. <strong>Repository</strong> abstrai a complexidade de acesso a dados, facilitando testes e mudanças de implementação. <strong>Observer</strong> desacopla componentes através de comunicação baseada em eventos, essencial para sistemas reativos. <strong>Strategy</strong> encapsula algoritmos intercambiáveis, tornando seu código extensível sem necessidade de modificações constantes.</p>

<p>O diferencial de um bom desenvolvedor não é memorizar padrões, mas reconhecer quando cada um resolve genuinamente um problema no código real. Use-os quando agregarem valor, não por usar. Python, sendo uma linguagem expressiva, frequentemente permite soluções elegantes que aproveitam esses padrões sem cerimônia excessiva.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://en.wikipedia.org/wiki/Design_Patterns" target="_blank" rel="noopener noreferrer">Design Patterns: Elements of Reusable Object-Oriented Software - Gang of Four</a></li>

<li><a href="https://realpython.com/design-patterns-python/" target="_blank" rel="noopener noreferrer">Real Python - Design Patterns in Python</a></li>

<li><a href="https://refactoring.guru/design-patterns" target="_blank" rel="noopener noreferrer">Refactoring Guru - Design Patterns</a></li>

<li><a href="https://docs.python.org/3/library/abc.html" target="_blank" rel="noopener noreferrer">Python Official Documentation - abc module</a></li>

<li><a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html" target="_blank" rel="noopener noreferrer">Clean Architecture by Robert C. Martin</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Python

FastAPI em Python: Fundamentos, Roteamento e Validação com Pydantic na Prática
FastAPI em Python: Fundamentos, Roteamento e Validação com Pydantic na Prática

Introdução ao FastAPI FastAPI é um framework web moderno para construir APIs...

Boas Práticas de Dataclasses em Python: @dataclass, fields e post_init para Times Ágeis
Boas Práticas de Dataclasses em Python: @dataclass, fields e post_init para Times Ágeis

O que são Dataclasses e Por Que Importam Dataclasses são uma ferramenta poder...

Dominando Testes de APIs Python: pytest com httpx e TestClient do FastAPI em Projetos Reais
Dominando Testes de APIs Python: pytest com httpx e TestClient do FastAPI em Projetos Reais

Por que Testar APIs com Python? Testar uma API é tão importante quanto desenv...