Design Patterns em Python na Pratica

Aprenda os principais design patterns em Python com exemplos praticos. Singleton, Factory, Observer, Strategy e mais padroes de projeto.

5 min de leitura Equipe Python Brasil

Design patterns sao solucoes testadas e comprovadas para problemas recorrentes no desenvolvimento de software. Em Python, muitos desses padroes podem ser implementados de forma elegante gracas aos recursos dinamicos da linguagem. Neste artigo, vamos explorar os padroes mais uteis com implementacoes praticas e pythonicas.

O Que Sao Design Patterns

Padroes de projeto nao sao codigo pronto para copiar e colar. Sao modelos conceituais que descrevem como resolver determinados tipos de problemas. Eles se dividem em tres categorias: criacionais (como criar objetos), estruturais (como compor objetos) e comportamentais (como objetos interagem).

Singleton: Instancia Unica

O Singleton garante que uma classe tenha apenas uma instancia em toda a aplicacao. Em Python, podemos implementa-lo de varias formas:

class DatabaseConnection:
    """Singleton usando __new__."""
    _instancia = None
    _inicializado = False

    def __new__(cls, *args, **kwargs):
        if cls._instancia is None:
            cls._instancia = super().__new__(cls)
        return cls._instancia

    def __init__(self, host: str = "localhost", porta: int = 5432):
        if not self._inicializado:
            self.host = host
            self.porta = porta
            self.conexoes_ativas = 0
            self._inicializado = True
            print(f"Conexao criada: {host}:{porta}")

    def conectar(self):
        self.conexoes_ativas += 1
        print(f"Conexao #{self.conexoes_ativas} estabelecida")

# Teste
db1 = DatabaseConnection("servidor-prod", 5432)
db2 = DatabaseConnection()

print(f"Mesma instancia? {db1 is db2}")  # True
print(f"Host: {db2.host}")  # servidor-prod

Uma alternativa mais pythonica e usar um modulo como singleton, ja que modulos Python sao importados apenas uma vez.

Factory Method: Criacao Flexivel

O Factory Method delega a criacao de objetos para subclasses, permitindo flexibilidade:

from abc import ABC, abstractmethod

class Notificacao(ABC):
    @abstractmethod
    def enviar(self, mensagem: str) -> bool:
        pass

class EmailNotificacao(Notificacao):
    def __init__(self, destinatario: str):
        self.destinatario = destinatario

    def enviar(self, mensagem: str) -> bool:
        print(f"Email para {self.destinatario}: {mensagem}")
        return True

class SMSNotificacao(Notificacao):
    def __init__(self, telefone: str):
        self.telefone = telefone

    def enviar(self, mensagem: str) -> bool:
        print(f"SMS para {self.telefone}: {mensagem}")
        return True

class PushNotificacao(Notificacao):
    def __init__(self, device_id: str):
        self.device_id = device_id

    def enviar(self, mensagem: str) -> bool:
        print(f"Push para {self.device_id}: {mensagem}")
        return True

class NotificacaoFactory:
    """Factory que cria a notificacao adequada."""
    _tipos = {
        "email": EmailNotificacao,
        "sms": SMSNotificacao,
        "push": PushNotificacao,
    }

    @classmethod
    def criar(cls, tipo: str, destino: str) -> Notificacao:
        classe = cls._tipos.get(tipo)
        if classe is None:
            raise ValueError(f"Tipo de notificacao desconhecido: {tipo}")
        return classe(destino)

# Uso
notif = NotificacaoFactory.criar("email", "user@email.com")
notif.enviar("Seu pedido foi confirmado!")

sms = NotificacaoFactory.criar("sms", "+5511999999999")
sms.enviar("Codigo de verificacao: 4821")

Observer: Eventos e Reacoes

O Observer permite que objetos sejam notificados quando algo muda, ideal para sistemas orientados a eventos:

from typing import Callable

class EventManager:
    """Gerenciador de eventos generico."""

    def __init__(self):
        self._assinantes: dict[str, list[Callable]] = {}

    def inscrever(self, evento: str, callback: Callable):
        if evento not in self._assinantes:
            self._assinantes[evento] = []
        self._assinantes[evento].append(callback)

    def desinscrever(self, evento: str, callback: Callable):
        if evento in self._assinantes:
            self._assinantes[evento].remove(callback)

    def notificar(self, evento: str, dados=None):
        for callback in self._assinantes.get(evento, []):
            callback(dados)

class Loja:
    """Loja que emite eventos."""

    def __init__(self):
        self.eventos = EventManager()

    def nova_venda(self, produto: str, valor: float):
        venda = {"produto": produto, "valor": valor}
        print(f"Venda realizada: {produto} - R$ {valor:.2f}")
        self.eventos.notificar("venda_realizada", venda)

    def estoque_baixo(self, produto: str, quantidade: int):
        dados = {"produto": produto, "quantidade": quantidade}
        self.eventos.notificar("estoque_baixo", dados)

# Funcoes observadoras
def enviar_nota_fiscal(dados):
    print(f"  Nota fiscal emitida para {dados['produto']}")

def atualizar_dashboard(dados):
    print(f"  Dashboard atualizado: +R$ {dados['valor']:.2f}")

def alertar_compras(dados):
    print(f"  Alerta: {dados['produto']} com apenas {dados['quantidade']} unidades!")

# Configurar
loja = Loja()
loja.eventos.inscrever("venda_realizada", enviar_nota_fiscal)
loja.eventos.inscrever("venda_realizada", atualizar_dashboard)
loja.eventos.inscrever("estoque_baixo", alertar_compras)

# Testar
loja.nova_venda("Notebook", 4599.90)
loja.estoque_baixo("Mouse", 3)

Strategy: Algoritmos Intercambiaveis

O Strategy permite trocar algoritmos em tempo de execucao:

from abc import ABC, abstractmethod
from dataclasses import dataclass

class DescontoStrategy(ABC):
    @abstractmethod
    def calcular(self, valor: float) -> float:
        pass

class SemDesconto(DescontoStrategy):
    def calcular(self, valor: float) -> float:
        return valor

class DescontoPercentual(DescontoStrategy):
    def __init__(self, percentual: float):
        self.percentual = percentual

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

class DescontoFixo(DescontoStrategy):
    def __init__(self, valor_desconto: float):
        self.valor_desconto = valor_desconto

    def calcular(self, valor: float) -> float:
        return max(0, valor - self.valor_desconto)

class DescontoProgressivo(DescontoStrategy):
    def calcular(self, valor: float) -> float:
        if valor > 1000:
            return valor * 0.85
        elif valor > 500:
            return valor * 0.90
        elif valor > 200:
            return valor * 0.95
        return valor

@dataclass
class Pedido:
    itens: list[tuple[str, float]]
    desconto: DescontoStrategy = None

    def __post_init__(self):
        if self.desconto is None:
            self.desconto = SemDesconto()

    @property
    def subtotal(self) -> float:
        return sum(preco for _, preco in self.itens)

    @property
    def total(self) -> float:
        return self.desconto.calcular(self.subtotal)

    def resumo(self):
        print(f"Subtotal: R$ {self.subtotal:.2f}")
        print(f"Total com desconto: R$ {self.total:.2f}")
        print(f"Economia: R$ {self.subtotal - self.total:.2f}")

# Uso
itens = [("Camisa", 89.90), ("Calca", 159.90), ("Tenis", 299.90)]

pedido = Pedido(itens, DescontoPercentual(15))
pedido.resumo()

print()
pedido_prog = Pedido(itens, DescontoProgressivo())
pedido_prog.resumo()

Decorator Pattern: Estendendo Funcionalidades

Nao confundir com decorators do Python. O padrao Decorator adiciona comportamento a objetos dinamicamente:

class Cafe:
    def custo(self) -> float:
        return 5.00

    def descricao(self) -> str:
        return "Cafe simples"

class CafeDecorator:
    def __init__(self, cafe: Cafe):
        self._cafe = cafe

    def custo(self) -> float:
        return self._cafe.custo()

    def descricao(self) -> str:
        return self._cafe.descricao()

class ComLeite(CafeDecorator):
    def custo(self) -> float:
        return self._cafe.custo() + 2.00

    def descricao(self) -> str:
        return self._cafe.descricao() + " + leite"

class ComChocolate(CafeDecorator):
    def custo(self) -> float:
        return self._cafe.custo() + 3.50

    def descricao(self) -> str:
        return self._cafe.descricao() + " + chocolate"

# Compor dinamicamente
meu_cafe = Cafe()
meu_cafe = ComLeite(meu_cafe)
meu_cafe = ComChocolate(meu_cafe)

print(f"{meu_cafe.descricao()}: R$ {meu_cafe.custo():.2f}")
# Cafe simples + leite + chocolate: R$ 10.50

Quando Usar Cada Padrao

Escolha o padrao certo para cada situacao. Use Singleton para recursos compartilhados como conexoes de banco. Use Factory quando a criacao de objetos depende de condicoes externas. Use Observer para desacoplar emissores e receptores de eventos. Use Strategy quando precisar alternar entre diferentes algoritmos. E use Decorator para adicionar funcionalidades sem modificar classes existentes.

O mais importante e nao forcar o uso de um padrao onde ele nao e necessario. Comece com codigo simples e aplique padroes apenas quando a complexidade justificar.

Conclusao

Design patterns sao ferramentas essenciais no arsenal de qualquer desenvolvedor Python. Eles promovem codigo reutilizavel, extensivel e facil de manter. Python, com seus recursos dinamicos, permite implementacoes elegantes e concisas desses padroes. Pratique cada um em projetos reais e voce vera como eles transformam a qualidade do seu codigo.

E

Equipe Python Brasil

Contribuidor do Python Brasil — Aprenda Python em Português