Python Admin

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

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

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 concreta? Em vez de usar new 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.

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

Implementação Prática com Factory Method

from abc import ABC, abstractmethod

# Classes abstratas que definem o contrato
class Pagamento(ABC):
    @abstractmethod
    def processar(self, valor: float) -> bool:
        pass

class CartaoCredito(Pagamento):
    def processar(self, valor: float) -> bool:
        print(f"Processando R$ {valor} no cartão de crédito")
        return True

class Boleto(Pagamento):
    def processar(self, valor: float) -> bool:
        print(f"Gerando boleto de R$ {valor}")
        return True

class PIX(Pagamento):
    def processar(self, valor: float) -> bool:
        print(f"Enviando PIX de R$ {valor}")
        return True

# Factory Method
class PagamentoFactory:
    _tipos = {
        'cartao': CartaoCredito,
        'boleto': Boleto,
        'pix': PIX
    }

    @staticmethod
    def criar(tipo: str) -> Pagamento:
        if tipo not in PagamentoFactory._tipos:
            raise ValueError(f"Tipo de pagamento '{tipo}' não suportado")
        return PagamentoFactory._tipos[tipo]()

# Uso
pagamento = PagamentoFactory.criar('pix')
pagamento.processar(150.00)

pagamento = PagamentoFactory.criar('cartao')
pagamento.processar(500.00)

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 _tipos. Não há necessidade de alterar o código que usa a factory.

Variação com Abstract Factory

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

from abc import ABC, abstractmethod

class BotaoUI(ABC):
    @abstractmethod
    def renderizar(self) -> str:
        pass

class CampoUI(ABC):
    @abstractmethod
    def renderizar(self) -> str:
        pass

class BotaoWindows(BotaoUI):
    def renderizar(self) -> str:
        return "Botão com estilo Windows"

class BotaoMac(BotaoUI):
    def renderizar(self) -> str:
        return "Botão com estilo Mac"

class CampoWindows(CampoUI):
    def renderizar(self) -> str:
        return "Campo com aparência Windows"

class CampoMac(CampoUI):
    def renderizar(self) -> str:
        return "Campo com aparência Mac"

class FactoryUI(ABC):
    @abstractmethod
    def criar_botao(self) -> BotaoUI:
        pass

    @abstractmethod
    def criar_campo(self) -> CampoUI:
        pass

class FactoryWindows(FactoryUI):
    def criar_botao(self) -> BotaoUI:
        return BotaoWindows()

    def criar_campo(self) -> CampoUI:
        return CampoWindows()

class FactoryMac(FactoryUI):
    def criar_botao(self) -> BotaoUI:
        return BotaoMac()

    def criar_campo(self) -> CampoUI:
        return CampoMac()

# Uso
def criar_interface(sistema_operacional: str):
    if sistema_operacional == 'windows':
        factory = FactoryWindows()
    else:
        factory = FactoryMac()

    botao = factory.criar_botao()
    campo = factory.criar_campo()

    print(botao.renderizar())
    print(campo.renderizar())

criar_interface('windows')

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

Repository Pattern: Abstraindo o Acesso a Dados

Por Que Abstrair Dados?

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.

Repository Genérico com Python

from abc import ABC, abstractmethod
from typing import List, Optional, TypeVar, Generic
import json
import os

T = TypeVar('T')

# Definição abstrata do repositório
class Repository(ABC, Generic[T]):
    @abstractmethod
    def adicionar(self, entidade: T) -> None:
        pass

    @abstractmethod
    def obter_por_id(self, id: int) -> Optional[T]:
        pass

    @abstractmethod
    def obter_todos(self) -> List[T]:
        pass

    @abstractmethod
    def atualizar(self, entidade: T) -> None:
        pass

    @abstractmethod
    def deletar(self, id: int) -> 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"Usuario(id={self.id}, nome='{self.nome}', email='{self.email}')"

# Implementação em memória (perfeita para testes)
class RepositorioUsuarioEmMemoria(Repository[Usuario]):
    def __init__(self):
        self._usuarios = {}

    def adicionar(self, usuario: Usuario) -> None:
        self._usuarios[usuario.id] = usuario

    def obter_por_id(self, id: int) -> Optional[Usuario]:
        return self._usuarios.get(id)

    def obter_todos(self) -> List[Usuario]:
        return list(self._usuarios.values())

    def atualizar(self, usuario: Usuario) -> None:
        if usuario.id in self._usuarios:
            self._usuarios[usuario.id] = usuario

    def deletar(self, id: int) -> 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 = "usuarios.json"):
        self.arquivo = arquivo
        self._garantir_arquivo()

    def _garantir_arquivo(self):
        if not os.path.exists(self.arquivo):
            with open(self.arquivo, 'w') as f:
                json.dump({}, f)

    def _carregar(self) -> dict:
        with open(self.arquivo, 'r') as f:
            return json.load(f)

    def _salvar(self, dados: dict):
        with open(self.arquivo, 'w') as f:
            json.dump(dados, f, indent=2)

    def adicionar(self, usuario: Usuario) -> None:
        dados = self._carregar()
        dados[str(usuario.id)] = {
            'id': usuario.id,
            'nome': usuario.nome,
            'email': usuario.email
        }
        self._salvar(dados)

    def obter_por_id(self, id: int) -> Optional[Usuario]:
        dados = self._carregar()
        if str(id) in dados:
            u = dados[str(id)]
            return Usuario(u['id'], u['nome'], u['email'])
        return None

    def obter_todos(self) -> List[Usuario]:
        dados = self._carregar()
        return [Usuario(u['id'], u['nome'], u['email']) for u in dados.values()]

    def atualizar(self, usuario: Usuario) -> None:
        dados = self._carregar()
        dados[str(usuario.id)] = {
            'id': usuario.id,
            'nome': usuario.nome,
            'email': usuario.email
        }
        self._salvar(dados)

    def deletar(self, id: int) -> 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) -> Usuario:
        usuario = Usuario(id, nome, email)
        self.repositorio.adicionar(usuario)
        return usuario

    def obter_usuario(self, id: int) -> Optional[Usuario]:
        return self.repositorio.obter_por_id(id)

    def listar_usuarios(self) -> List[Usuario]:
        return self.repositorio.obter_todos()

# Uso
if __name__ == "__main__":
    # Teste com repositório em memória
    repo_memoria = RepositorioUsuarioEmMemoria()
    servico = ServicoUsuario(repo_memoria)

    servico.registrar_usuario(1, "João Silva", "joao@example.com")
    servico.registrar_usuario(2, "Maria Santos", "maria@example.com")

    print("Usuários em memória:", servico.listar_usuarios())

    # Se precisar trocar para JSON, apenas uma linha muda:
    repo_json = RepositorioUsuarioJSON()
    servico = ServicoUsuario(repo_json)
    servico.registrar_usuario(3, "Pedro Costa", "pedro@example.com")
    print("Usuários em JSON:", servico.listar_usuarios())

Veja como a classe ServicoUsuario 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 Repository, permitindo que você mude a implementação conforme necessário, inclusive para testes.

Observer Pattern: Comunicação Reativa Entre Objetos

Entendendo o Fluxo de Eventos

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 "mudou?"), o Observer implementa push: quando algo muda, notifica interessados automaticamente. É amplamente usado em interfaces gráficas, sistemas de eventos e arquiteturas reativas.

Implementação Clássica

from abc import ABC, abstractmethod
from typing import List

# Interface para observadores
class Observador(ABC):
    @abstractmethod
    def atualizar(self, evento: str, dados: dict) -> 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) -> None:
        if observador not in self._observadores:
            self._observadores.append(observador)

    def desanexar(self, observador: Observador) -> None:
        if observador in self._observadores:
            self._observadores.remove(observador)

    def _notificar(self, evento: str, dados: dict) -> None:
        for observador in self._observadores:
            observador.atualizar(evento, dados)

    def depositar(self, valor: float) -> None:
        self._saldo += valor
        self._notificar('deposito', {
            'valor': valor,
            'saldo_novo': self._saldo
        })

    def sacar(self, valor: float) -> None:
        if valor <= self._saldo:
            self._saldo -= valor
            self._notificar('saque', {
                'valor': valor,
                'saldo_novo': self._saldo
            })
        else:
            self._notificar('saque_rejeitado', {
                'valor': valor,
                'saldo_disponivel': self._saldo
            })

    @property
    def saldo(self) -> float:
        return self._saldo

# Observadores concretos
class LogTransacao(Observador):
    def atualizar(self, evento: str, dados: dict) -> None:
        print(f"[LOG] Evento: {evento} | Dados: {dados}")

class NotificacaoEmail(Observador):
    def atualizar(self, evento: str, dados: dict) -> None:
        if evento in ['deposito', 'saque']:
            print(f"[EMAIL] Transação realizada: {evento}")
            print(f"[EMAIL] Novo saldo: R$ {dados.get('saldo_novo')}")

class AlertaSaldoBaixo(Observador):
    def __init__(self, limite: float = 100):
        self.limite = limite

    def atualizar(self, evento: str, dados: dict) -> None:
        saldo = dados.get('saldo_novo')
        if saldo is not None and saldo < self.limite:
            print(f"[ALERTA] Saldo baixo! R$ {saldo} < R$ {self.limite}")

# Uso
conta = Conta(1000)

log = LogTransacao()
email = NotificacaoEmail()
alerta = AlertaSaldoBaixo(200)

conta.anexar(log)
conta.anexar(email)
conta.anexar(alerta)

print("=== Depósito ===")
conta.depositar(500)

print("\n=== Saque pequeno ===")
conta.sacar(100)

print("\n=== Saque grande ===")
conta.sacar(1200)

print(f"\nSaldo final: R$ {conta.saldo}")

Note que adicionar novos observadores não afeta a classe Conta. 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.

Observer com Decoradores (Pythônico)

Python permite uma abordagem mais elegante usando decoradores:

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):
        """Decorador para registrar handlers de eventos"""
        def decorador(func: Callable) -> 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) -> None:
        """Dispara um evento para todos os listeners"""
        if evento in self._listeners:
            for handler in self._listeners[evento]:
                handler(**dados)

# Uso com decoradores
gerenciador = EventManager()

@gerenciador.on('usuario_criado')
def enviar_email_boas_vindas(usuario_id: int, email: str, **kwargs):
    print(f"[EMAIL] Bem-vindo {email}!")

@gerenciador.on('usuario_criado')
def registrar_audit(usuario_id: int, **kwargs):
    print(f"[AUDIT] Novo usuário criado: {usuario_id}")

# Dispara o evento
gerenciador.emit('usuario_criado', usuario_id=1, email='novo@example.com')

Strategy Pattern: Intercambiando Algoritmos em Tempo de Execução

O Problema de Múltiplos Caminhos

Frequentemente, você precisa executar diferentes algoritmos baseado em condições. A abordagem ingênua é encher seu código com if/elif/else. 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.

Implementação Prática com Cálculo de Preços

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) -> float:
        pass

class SemDesconto(EstrategiaDesconto):
    def calcular_desconto(self, valor: float) -> float:
        return 0

class DescontoFixo(EstrategiaDesconto):
    def __init__(self, percentual: float):
        self.percentual = percentual

    def calcular_desconto(self, valor: float) -> float:
        return valor * (self.percentual / 100)

class DescontoProgressivo(EstrategiaDesconto):
    """Quanto maior a compra, maior o desconto"""
    def calcular_desconto(self, valor: float) -> float:
        if valor > 1000:
            return valor * 0.20
        elif valor > 500:
            return valor * 0.10
        elif valor > 100:
            return valor * 0.05
        return 0

class DescontoParaCliente(EstrategiaDesconto):
    """Black Friday ou oferta especial"""
    def calcular_desconto(self, valor: float) -> float:
        # Simula ofertas em horários específicos
        hora_atual = datetime.now().hour
        if 20 <= hora_atual <= 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) -> None:
        self.estrategia = estrategia

    def calcular_total(self) -> dict:
        subtotal = sum(item['preco'] * item['quantidade'] for item in self.items)
        desconto = self.estrategia.calcular_desconto(subtotal)
        total = subtotal - desconto

        return {
            'subtotal': subtotal,
            'desconto': desconto,
            'total': total,
            'estrategia': self.estrategia.__class__.__name__
        }

# Uso
items = [
    {'preco': 100, 'quantidade': 2},
    {'preco': 150, 'quantidade': 3}
]

# Cliente normal, sem desconto
pedido = Pedido(items)
print("Sem desconto:", pedido.calcular_total())

# Cliente especial com desconto fixo
pedido.mudar_estrategia(DescontoFixo(10))
print("Com 10% fixo:", pedido.calcular_total())

# Cliente que fez uma grande compra
pedido.mudar_estrategia(DescontoProgressivo())
print("Com desconto progressivo:", pedido.calcular_total())

# Black Friday
pedido.mudar_estrategia(DescontoParaCliente())
print("Black Friday:", pedido.calcular_total())

Strategy com Funções (Mais Simples)

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

from typing import Callable

class ProcessadorPagamento:
    def __init__(self, estrategia_taxa: Callable[[float], float]):
        self.estrategia_taxa = estrategia_taxa

    def processar(self, valor: float) -> dict:
        taxa = self.estrategia_taxa(valor)
        total = valor + taxa
        return {
            'valor_original': valor,
            'taxa': taxa,
            'total_cobrado': total
        }

# Estratégias como funções simples
def taxa_cartao_credito(valor: float) -> float:
    return valor * 0.03

def taxa_boleto(valor: float) -> float:
    return 3.50  # Taxa fixa

def taxa_pix(valor: float) -> 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))

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

Conclusão

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

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.

Referências


Artigos relacionados