Table of Contents

Os padrões de design representam soluções testadas no tempo para desafios recorrentes no desenvolvimento de software. Na engenharia Python, essas soluções reutilizáveis ajudam os desenvolvedores a criar código mais mantenedor, flexível e escalável. Compreender e implementar padrões de design de forma eficaz pode transformar a forma como você aborda a arquitetura de software, permitindo que você escreva código que não é apenas funcional, mas também elegante, reutilizável e mais fácil de manter ao longo do tempo.

Os padrões de design servem como um vocabulário que permite aos engenheiros comunicarem decisões estruturais de forma concisa. Quando um desenvolvedor sênior sugere usar um padrão específico, eles estão transmitindo uma abordagem arquitetônica inteira em apenas algumas palavras. Esta linguagem compartilhada acelera a colaboração da equipe e garante que todos entendam a estrutura subjacente da base de código.

Compreendendo os padrões de design no contexto Python

Python é uma linguagem de programação de alto nível com digitação dinâmica e ligação dinâmica, tornando-a uma linguagem dinâmica poderosa e de alto nível. Esta flexibilidade dá vantagens únicas ao Python ao implementar padrões de design, mas também significa que alguns padrões precisam ser adaptados para se ajustar às expressões e capacidades do Python.

Tudo em Python é um objeto, incluindo funções, que são objetos de primeira classe. Esta característica fundamental influencia como padrões de design são implementados em Python em comparação com linguagens mais rígidas e com tipos estáticos. A natureza dinâmica do Python permite implementações mais concisas de muitos padrões, ao mesmo tempo que introduz a necessidade de práticas de codificação disciplinadas.

Porque Python é tão poderoso e flexível, precisamos de algumas regras ou padrões quando programamos nele. Sem estes princípios orientadores, as bases de código podem rapidamente tornar-se descontroladas e difíceis de manter. Os padrões de design fornecem a estrutura necessária para aproveitar o vasto potencial do Python, mantendo a qualidade do código e a legibilidade.

As Três Categorias de Padrões de Desenho

Os padrões de design são tradicionalmente organizados em três categorias principais, cada uma abordando diferentes aspectos do design de software. Compreender essas categorias ajuda os desenvolvedores a selecionar o padrão adequado para seus desafios específicos.

Padrões Criacionais

Os padrões de criação focam no processo de criação de objetos, fornecendo mecanismos que aumentam a flexibilidade e a reutilização do código existente. Esses padrões abstraem o processo de instanciação, tornando os sistemas independentes de como os objetos são criados, compostos e representados.

Em vez de criar objetos diretamente usando um construtor, padrões de criação fornecem mais controle e flexibilidade sobre o processo de criação. Esta abordagem é particularmente valiosa quando a lógica de criação é complexa, quando você precisa controlar qual classe é instanciada, ou quando você deseja gerenciar alocação de recursos de forma mais eficiente.

Padrões de criação comuns em Python incluem Singleton, Factory Method, Abstract Factory, Builder e Prototype. Cada um serve um propósito distinto na gestão da complexidade da criação de objetos.

Padrões estruturais

Os padrões de design estrutural focam na composição de classes ou objetos para formar estruturas maiores e mais complexas, ajudando a organizar e gerenciar relações entre objetos para alcançar maior flexibilidade, reutilização e manutenção. Esses padrões estão preocupados com a forma como classes e objetos são compostos para formar estruturas maiores, mantendo essas estruturas flexíveis e eficientes.

Os padrões estruturais ajudam a garantir que quando uma parte de um sistema muda, toda a estrutura não precisa ser modificada. Eles facilitam o design de sistemas onde os componentes podem ser facilmente substituídos ou estendidos sem afetar outras partes da aplicação. Os padrões estruturais comuns incluem Adaptador, Ponte, Composto, Decorador, Faca, Peso Voador e Proxy.

Padrões Comportamentais

Os padrões comportamentais estão preocupados com algoritmos e a atribuição de responsabilidades entre objetos. Eles descrevem não apenas padrões de objetos ou classes, mas também os padrões de comunicação entre eles. Esses padrões caracterizam o fluxo de controle complexo que é difícil de seguir no tempo de execução.

Observador aborda a comunicação, permitindo que várias partes de um sistema reajam automaticamente a eventos ou mudanças de estado. Outros padrões comportamentais incluem Estratégia, Comando, Iterador, Mediador, Memória, Estado, Método de Modelo, Visitante e Cadeia de Responsabilidade.

O padrão de singleton: uma instância para governar todos eles

O Padrão Singleton garante que uma classe tenha apenas uma instância ao longo de um programa e fornece um ponto de acesso global, comumente usado para gerenciar recursos compartilhados, como bancos de dados, sistemas de registro ou gerenciadores de arquivos. Este padrão é um dos padrões mais discutidos e às vezes controversos no desenvolvimento de software.

Quando usar o Singleton

O padrão Singleton é particularmente útil quando você precisa exatamente de uma instância de uma classe para controlar recursos, como conexões de banco de dados, gerenciadores de configuração ou sistemas de registro. O padrão garante que todo código usando a instância de classe está trabalhando com o mesmo objeto, garantindo consistência em toda a aplicação.

Casos legítimos de uso para singletons em Python incluem:

  • Interfaces de hardware que representam recursos físicos únicos, como uma câmera, uma impressora ou uma interface GPIO, onde um únicoton modela com precisão
  • Cache camadas onde você deseja um único cache compartilhado em toda sua aplicação
  • Piscinas de thread ou piscinas de conexão onde você deseja limitar e compartilhar recursos caros, com o pool em si sendo um singleton embora os recursos que ele gerencia não sejam
  • Sistemas de gerenciamento de configuração que precisam manter configurações consistentes ao longo da aplicação
  • Sistemas de registro onde o gerenciamento centralizado de logs é essencial

Implementação de Singleton em Python

A classe Singleton pode ser implementada de diferentes maneiras em Python, incluindo abordagens de classe base, decorador e metaclasse, sendo a metaclasse mais adequada para esse fim. Cada método de implementação tem suas próprias vantagens e trade-offs.

A implementação clássica usando o método new é assim:

class DatabaseConnection:
 _instance = None

 def __new__(cls):
 if cls._instance is None:
 cls._instance = super(DatabaseConnection, cls).__new__(cls)
 cls._instance._initialize()
 return cls._instance

 def _initialize(self):
 self.connection = None
 self.host = "localhost"
 self.port = 5432

 def connect(self):
 if not self.connection:
 self.connection = f"Connected to {self.host}:{self.port}"
 return self.connection

Uma implementação segura de thread usa um objeto de bloqueio que sincroniza threads durante o primeiro acesso ao Singleton. Isto é crucial em aplicações multi-threads onde as condições de corrida podem levar a várias instâncias sendo criadas:

from threading import Lock

class ThreadSafeSingleton:
 _instance = None
 _lock = Lock()

 def __new__(cls):
 if cls._instance is None:
 with cls._lock:
 if cls._instance is None:
 cls._instance = super().__new__(cls)
 return cls._instance

A alternativa Pythonic: Módulo-Nível Singletons

O sistema de módulos Python é em si um mecanismo singleton: quando você importa um módulo, o Python executa- o uma vez e armazena o resultado em sys.modules, com cada importação subsequente retornando o objeto do módulo cache, não um novo. Isto torna os módulos uma maneira natural e Pythonic de implementar o comportamento de singleton.

O Python importa um módulo apenas uma vez, tornando tudo definido dentro de um módulo efetivamente um singleton. Esta abordagem é muitas vezes mais simples e mais mantendível do que implementar uma classe formal de Singleton:

# config.py
class _Config:
 def __init__(self):
 self.database_url = "postgresql://localhost/mydb"
 self.api_key = "secret_key"
 self.debug_mode = True

 def update_setting(self, key, value):
 setattr(self, key, value)

# Create the singleton instance
config = _Config()

# In other files, simply import:
# from config import config

O padrão singleton geralmente não faz sentido em Python em sua forma mais pura; em vez disso, geralmente faz mais sentido para fazer uma única instância de uma classe e atribuir essa instância a uma variável global em um módulo. Esta abordagem é mais transparente, mais fácil de testar, e se alinha melhor com a filosofia de Python.

Retrocessos de Singleton e Considerações

Muitos desenvolvedores consideram o padrão Singleton um antipadrão, razão pela qual seu uso está em declínio no código Python. Existem várias preocupações legítimas:

  • Devido ao seu estado global e acoplamento apertado com outras partes da base de código, o Singleton Pattern pode tornar o teste de unidade desafiador, com zombaria ou substituição da instância singleton sendo complicado
  • Em ambientes multithreaded, o Singleton Pattern pode introduzir problemas de concorrência se não for implementado com cuidado, com múltiplos threads potencialmente criando múltiplas instâncias sem sincronização adequada
  • Singletons criam dependências ocultas que tornam o código mais difícil de entender e manter
  • Eles violam o Princípio da Responsabilidade Única, gerenciando tanto a lógica de negócios quanto a instanciação
  • Singletons dificultam a extensão ou modificação do comportamento através da herança

Considere usar injeção de dependência, pois poderia ser mais limpa, ou usar uma instância de nível de módulo. Essas alternativas muitas vezes fornecem os mesmos benefícios sem os inconvenientes.

Padrão de fábrica: Criação de objetos flexíveis

O padrão Fábrica fornece uma interface para criar objetos sem especificar suas classes exatas, promovendo acoplamento solto e tornando o código mais flexível às mudanças. Este padrão é inestimável quando você precisa criar objetos, mas deseja dissociar a lógica de criação do código que usa esses objetos.

A fábrica concentra-se em como os objetos são criados, escondendo lógica de instanciação e reduzindo o acoplamento apertado entre os componentes. Ao centralizar a criação de objetos, o padrão de Fábrica facilita a introdução de novos tipos sem modificar o código existente.

Implementação Simples de Fábrica

O padrão de Fábrica Simples encapsula a criação de objetos em uma função ou classe dedicada. Aqui está um exemplo prático para criar diferentes tipos de conexões de banco de dados:

from abc import ABC, abstractmethod

class Database(ABC):
 @abstractmethod
 def connect(self):
 pass

 @abstractmethod
 def query(self, sql):
 pass

class PostgreSQLDatabase(Database):
 def connect(self):
 return "Connected to PostgreSQL"

 def query(self, sql):
 return f"PostgreSQL executing: {sql}"

class MySQLDatabase(Database):
 def connect(self):
 return "Connected to MySQL"

 def query(self, sql):
 return f"MySQL executing: {sql}"

class MongoDBDatabase(Database):
 def connect(self):
 return "Connected to MongoDB"

 def query(self, sql):
 return f"MongoDB executing: {sql}"

class DatabaseFactory:
 @staticmethod
 def create_database(db_type):
 databases = {
 'postgresql': PostgreSQLDatabase,
 'mysql': MySQLDatabase,
 'mongodb': MongoDBDatabase
 }

 db_class = databases.get(db_type.lower())
 if not db_class:
 raise ValueError(f"Unknown database type: {db_type}")

 return db_class()

# Usage
db = DatabaseFactory.create_database('postgresql')
print(db.connect())
print(db.query("SELECT * FROM users"))

Padrão do Método de Fábrica

O Método de Fábrica fornece uma interface para criar objetos em uma superclasse, mas permite que subclasses alterem o tipo de objetos que serão criados. Esta variação dá ainda mais flexibilidade, delegando a instanciação às subclasses:

from abc import ABC, abstractmethod

class DocumentProcessor(ABC):
 @abstractmethod
 def create_parser(self):
 """Factory method to be implemented by subclasses"""
 pass

 def process_document(self, content):
 parser = self.create_parser()
 return parser.parse(content)

class Parser(ABC):
 @abstractmethod
 def parse(self, content):
 pass

class JSONParser(Parser):
 def parse(self, content):
 return f"Parsing JSON: {content}"

class XMLParser(Parser):
 def parse(self, content):
 return f"Parsing XML: {content}"

class CSVParser(Parser):
 def parse(self, content):
 return f"Parsing CSV: {content}"

class JSONDocumentProcessor(DocumentProcessor):
 def create_parser(self):
 return JSONParser()

class XMLDocumentProcessor(DocumentProcessor):
 def create_parser(self):
 return XMLParser()

# Usage
json_processor = JSONDocumentProcessor()
result = json_processor.process_document('{"name": "John"}')
print(result)

Benefícios do padrão de fábrica

O padrão de fábrica oferece várias vantagens convincentes:

  • Acoplamento solto: O código do cliente não precisa saber as classes de concreto sendo instanciadas
  • Responsabilidade única: A lógica de criação de objetos é centralizada em um só lugar
  • Princípio Aberto/Fechado: Novos tipos podem ser adicionados sem modificar o código existente
  • Flexibilidade: Fácil de alternar entre diferentes implementações em tempo de execução
  • Testabilidade: Os objetos de brincar podem ser facilmente injetados para fins de ensaio

Padrão do Observador: Arquitetura conduzida por eventos

O padrão Observer define um mecanismo de assinatura para notificar vários objetos sobre quaisquer eventos que ocorram com o objeto que eles estão observando. Este padrão é fundamental para a programação orientada por eventos e é amplamente utilizado em frameworks GUI, sistemas em tempo real e aplicações reativas.

Compreender o Padrão do Observador

O padrão Observer estabelece uma dependência de um para o outro entre objetos. Quando o sujeito (observado) muda de estado, todos os seus dependentes (observadores) são notificados e atualizados automaticamente. Isto desacopla o assunto dos seus observadores, permitindo- lhes variar de forma independente.

Aqui está uma implementação abrangente do padrão Observer:

from abc import ABC, abstractmethod
from typing import List

class Observer(ABC):
 @abstractmethod
 def update(self, subject):
 """Receive update from subject"""
 pass

class Subject(ABC):
 def __init__(self):
 self._observers: List[Observer] = []

 def attach(self, observer: Observer):
 """Attach an observer to the subject"""
 if observer not in self._observers:
 self._observers.append(observer)

 def detach(self, observer: Observer):
 """Detach an observer from the subject"""
 try:
 self._observers.remove(observer)
 except ValueError:
 pass

 def notify(self):
 """Notify all observers about an event"""
 for observer in self._observers:
 observer.update(self)

class StockPrice(Subject):
 def __init__(self, symbol: str, price: float):
 super().__init__()
 self._symbol = symbol
 self._price = price

 @property
 def symbol(self):
 return self._symbol

 @property
 def price(self):
 return self._price

 @price.setter
 def price(self, new_price: float):
 if new_price != self._price:
 self._price = new_price
 self.notify()

class StockDisplay(Observer):
 def __init__(self, name: str):
 self._name = name

 def update(self, subject: StockPrice):
 print(f"{self._name}: {subject.symbol} is now ${subject.price:.2f}")

class StockAlert(Observer):
 def __init__(self, threshold: float):
 self._threshold = threshold

 def update(self, subject: StockPrice):
 if subject.price > self._threshold:
 print(f"ALERT: {subject.symbol} exceeded ${self._threshold}! Current: ${subject.price:.2f}")

# Usage
apple_stock = StockPrice("AAPL", 150.00)

display1 = StockDisplay("Display 1")
display2 = StockDisplay("Display 2")
alert = StockAlert(160.00)

apple_stock.attach(display1)
apple_stock.attach(display2)
apple_stock.attach(alert)

apple_stock.price = 155.50
apple_stock.price = 162.00

Aplicações do Mundo Real

O padrão Observer é amplamente utilizado no desenvolvimento de software moderno:

  • GUI Event Handling: Cliques de botões, movimentos do mouse e eventos de teclado
  • Modelo-View-Controller (MVC): Vistas observam modelos para alterações de dados
  • Real-time Data Feeds: Preços das ações, atualizações meteorológicas, notificações de mídia social
  • Sistemas de registo: Múltiplos manipuladores de registo a observar eventos de aplicação
  • Sistemas de subscrição de publicação: filas de mensagens e autocarros de eventos

Padrão de decoração: Extendendo Funcionalidade Dinâmica

O padrão Decorator permite adicionar comportamento a um objeto – logging, caching, autenticação, retrying – sem modificar a classe do objeto e sem subclassing. Este padrão fornece uma alternativa flexível à subclassificação para estender a funcionalidade.

Decoradores Python vs Padrão de Decoração

A sintaxe @decorator do Python e o padrão de design do Decorator são conceitualmente os mesmos – ambos os comportamentos de wrap em torno de um existente callable sem modificá-lo, com a sintaxe do Python tornando o padrão uma característica nativa de linguagem. Isto torna o Python particularmente adequado para implementar a funcionalidade de decorador.

Aqui está um exemplo usando a sintaxe de decorador do Python:

import time
import functools

def timing_decorator(func):
 @functools.wraps(func)
 def wrapper(*args, **kwargs):
 start_time = time.time()
 result = func(*args, **kwargs)
 end_time = time.time()
 print(f"{func.__name__} took {end_time - start_time:.4f} seconds")
 return result
 return wrapper

def cache_decorator(func):
 cache = {}

 @functools.wraps(func)
 def wrapper(*args):
 if args in cache:
 print(f"Returning cached result for {args}")
 return cache[args]
 result = func(*args)
 cache[args] = result
 return result
 return wrapper

def retry_decorator(max_attempts=3):
 def decorator(func):
 @functools.wraps(func)
 def wrapper(*args, **kwargs):
 for attempt in range(max_attempts):
 try:
 return func(*args, **kwargs)
 except Exception as e:
 if attempt == max_attempts - 1:
 raise
 print(f"Attempt {attempt + 1} failed: {e}. Retrying...")
 return None
 return wrapper
 return decorator

@timing_decorator
@cache_decorator
def fibonacci(n):
 if n < 2:
 return n
 return fibonacci(n-1) + fibonacci(n-2)

@retry_decorator(max_attempts=3)
def unreliable_api_call():
 import random
 if random.random() < 0.7:
 raise ConnectionError("API temporarily unavailable")
 return "Success!"

# Usage
print(fibonacci(10))
print(fibonacci(10)) # This will use cached result

Padrão de Decoração com Classe

O padrão tradicional de decoração também pode ser implementado usando classes, que é útil para cenários mais complexos:

from abc import ABC, abstractmethod

class Coffee(ABC):
 @abstractmethod
 def cost(self):
 pass

 @abstractmethod
 def description(self):
 pass

class SimpleCoffee(Coffee):
 def cost(self):
 return 2.00

 def description(self):
 return "Simple coffee"

class CoffeeDecorator(Coffee):
 def __init__(self, coffee: Coffee):
 self._coffee = coffee

 def cost(self):
 return self._coffee.cost()

 def description(self):
 return self._coffee.description()

class MilkDecorator(CoffeeDecorator):
 def cost(self):
 return self._coffee.cost() + 0.50

 def description(self):
 return self._coffee.description() + ", milk"

class SugarDecorator(CoffeeDecorator):
 def cost(self):
 return self._coffee.cost() + 0.25

 def description(self):
 return self._coffee.description() + ", sugar"

class WhippedCreamDecorator(CoffeeDecorator):
 def cost(self):
 return self._coffee.cost() + 0.75

 def description(self):
 return self._coffee.description() + ", whipped cream"

# Usage
coffee = SimpleCoffee()
print(f"{coffee.description()}: ${coffee.cost():.2f}")

coffee_with_milk = MilkDecorator(coffee)
print(f"{coffee_with_milk.description()}: ${coffee_with_milk.cost():.2f}")

fancy_coffee = WhippedCreamDecorator(SugarDecorator(MilkDecorator(SimpleCoffee())))
print(f"{fancy_coffee.description()}: ${fancy_coffee.cost():.2f}")

Padrão de estratégia: Algoritmos intercambiáveis

O Padrão de Estratégia permite- lhe definir uma família de algoritmos, encapsular cada um e torná- los intercambiáveis em tempo de execução, delegar o comportamento para separar classes ou funções de estratégia em vez de escrever grandes blocos condicionais. Este padrão é essencial para escrever um código flexível e mantendível que possa adaptar- se a diferentes requisitos.

O padrão de estratégia define uma família de algoritmos, coloca cada um deles numa classe separada e torna os seus objectos intercambiáveis. Isto permite seleccionar algoritmos em tempo de execução com base no contexto ou configuração.

Padrões de estratégia de implementação

Aqui está um exemplo abrangente demonstrando o padrão de estratégia para o processamento de pagamentos:

from abc import ABC, abstractmethod
from typing import Protocol

class PaymentStrategy(Protocol):
 def pay(self, amount: float) -> str:
 """Process payment and return confirmation"""
 ...

class CreditCardPayment:
 def __init__(self, card_number: str, cvv: str, expiry: str):
 self.card_number = card_number
 self.cvv = cvv
 self.expiry = expiry

 def pay(self, amount: float) -> str:
 # Simulate payment processing
 masked_card = f"****-****-****-{self.card_number[-4:]}"
 return f"Paid ${amount:.2f} using Credit Card {masked_card}"

class PayPalPayment:
 def __init__(self, email: str):
 self.email = email

 def pay(self, amount: float) -> str:
 return f"Paid ${amount:.2f} using PayPal account {self.email}"

class CryptocurrencyPayment:
 def __init__(self, wallet_address: str, currency: str = "BTC"):
 self.wallet_address = wallet_address
 self.currency = currency

 def pay(self, amount: float) -> str:
 return f"Paid ${amount:.2f} using {self.currency} to wallet {self.wallet_address[:10]}..."

class BankTransferPayment:
 def __init__(self, account_number: str, routing_number: str):
 self.account_number = account_number
 self.routing_number = routing_number

 def pay(self, amount: float) -> str:
 masked_account = f"****{self.account_number[-4:]}"
 return f"Paid ${amount:.2f} via bank transfer from account {masked_account}"

class ShoppingCart:
 def __init__(self):
 self.items = []
 self.payment_strategy = None

 def add_item(self, item: str, price: float):
 self.items.append({'item': item, 'price': price})

 def set_payment_strategy(self, strategy: PaymentStrategy):
 self.payment_strategy = strategy

 def calculate_total(self) -> float:
 return sum(item['price'] for item in self.items)

 def checkout(self) -> str:
 if not self.payment_strategy:
 raise ValueError("Payment strategy not set")

 total = self.calculate_total()
 return self.payment_strategy.pay(total)

# Usage
cart = ShoppingCart()
cart.add_item("Laptop", 999.99)
cart.add_item("Mouse", 29.99)
cart.add_item("Keyboard", 79.99)

# Pay with credit card
cart.set_payment_strategy(CreditCardPayment("1234567890123456", "123", "12/25"))
print(cart.checkout())

# Change strategy to PayPal
cart.set_payment_strategy(PayPalPayment("[email protected]"))
print(cart.checkout())

# Change strategy to Cryptocurrency
cart.set_payment_strategy(CryptocurrencyPayment("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"))
print(cart.checkout())

Estratégia Pythonic com Funções

As funções de primeira classe do Python permitem uma implementação mais concisa do padrão de estratégia:

def discount_none(total):
 return total

def discount_percentage(percentage):
 def apply_discount(total):
 return total * (1 - percentage / 100)
 return apply_discount

def discount_fixed_amount(amount):
 def apply_discount(total):
 return max(0, total - amount)
 return apply_discount

def discount_bulk(threshold, discount_percentage):
 def apply_discount(total):
 if total >= threshold:
 return total * (1 - discount_percentage / 100)
 return total
 return apply_discount

class Order:
 def __init__(self, total: float):
 self.total = total
 self.discount_strategy = discount_none

 def set_discount_strategy(self, strategy):
 self.discount_strategy = strategy

 def calculate_final_price(self):
 return self.discount_strategy(self.total)

# Usage
order = Order(100.00)
print(f"No discount: ${order.calculate_final_price():.2f}")

order.set_discount_strategy(discount_percentage(10))
print(f"10% discount: ${order.calculate_final_price():.2f}")

order.set_discount_strategy(discount_fixed_amount(15))
print(f"$15 off: ${order.calculate_final_price():.2f}")

order.set_discount_strategy(discount_bulk(50, 20))
print(f"Bulk discount: ${order.calculate_final_price():.2f}")

Padrão do Construtor: Construindo Objetos Complexos

O padrão do Construtor permite- lhe construir objetos complexos passo a passo, permitindo- lhe produzir diferentes tipos e representações de um objeto usando o mesmo código de construção. Este padrão é particularmente útil quando um objeto requer inúmeras opções de configuração ou quando o processo de construção envolve várias etapas.

Quando usar o padrão do construtor

O padrão do Construtor brilha em cenários onde:

  • A construção de objetos requer muitos parâmetros opcionais
  • O processo de construção deve seguir uma sequência específica
  • Você precisa criar diferentes representações do mesmo objeto
  • Parâmetros do construtor criariam um anti- padrão "telescoping constructor"
  • A criação de objetos envolve lógica complexa que deve ser separada do próprio objeto

Padrões de construção de implementação em Python

Aqui está uma implementação prática para construir objetos de consulta de banco de dados:

from typing import List, Optional
from dataclasses import dataclass, field

@dataclass
class Query:
 table: str
 columns: List[str]
 where_clauses: List[str]
 joins: List[str]
 order_by: Optional[str]
 limit: Optional[int]
 offset: Optional[int]

 def to_sql(self) -> str:
 # Select clause
 cols = ", ".join(self.columns) if self.columns else "*"
 sql = f"SELECT {cols} FROM {self.table}"

 # Joins
 if self.joins:
 sql += " " + " ".join(self.joins)

 # Where clause
 if self.where_clauses:
 sql += " WHERE " + " AND ".join(self.where_clauses)

 # Order by
 if self.order_by:
 sql += f" ORDER BY {self.order_by}"

 # Limit and offset
 if self.limit:
 sql += f" LIMIT {self.limit}"
 if self.offset:
 sql += f" OFFSET {self.offset}"

 return sql

class QueryBuilder:
 def __init__(self):
 self._table: Optional[str] = None
 self._columns: List[str] = []
 self._where_clauses: List[str] = []
 self._joins: List[str] = []
 self._order_by: Optional[str] = None
 self._limit: Optional[int] = None
 self._offset: Optional[int] = None

 def table(self, table_name: str) -> 'QueryBuilder':
 self._table = table_name
 return self

 def select(self, *columns: str) -> 'QueryBuilder':
 self._columns.extend(columns)
 return self

 def where(self, condition: str) -> 'QueryBuilder':
 self._where_clauses.append(condition)
 return self

 def join(self, join_clause: str) -> 'QueryBuilder':
 self._joins.append(join_clause)
 return self

 def order_by(self, column: str, direction: str = "ASC") -> 'QueryBuilder':
 self._order_by = f"{column} {direction}"
 return self

 def limit(self, limit: int) -> 'QueryBuilder':
 self._limit = limit
 return self

 def offset(self, offset: int) -> 'QueryBuilder':
 self._offset = offset
 return self

 def build(self) -> Query:
 if not self._table:
 raise ValueError("Table name is required")

 return Query(
 table=self._table,
 columns=self._columns,
 where_clauses=self._where_clauses,
 joins=self._joins,
 order_by=self._order_by,
 limit=self._limit,
 offset=self._offset
 )

 def reset(self) -> 'QueryBuilder':
 self.__init__()
 return self

# Usage
builder = QueryBuilder()

# Build a simple query
query1 = (builder
 .table("users")
 .select("id", "name", "email")
 .where("age > 18")
 .where("status = 'active'")
 .order_by("name", "ASC")
 .limit(10)
 .build())

print(query1.to_sql())

# Build a complex query with joins
builder.reset()
query2 = (builder
 .table("orders")
 .select("orders.id", "users.name", "products.title", "orders.total")
 .join("INNER JOIN users ON orders.user_id = users.id")
 .join("INNER JOIN products ON orders.product_id = products.id")
 .where("orders.status = 'completed'")
 .where("orders.total > 100")
 .order_by("orders.created_at", "DESC")
 .limit(20)
 .offset(10)
 .build())

print(query2.to_sql())

Benefícios da interface fluente

O padrão Builder muitas vezes implementa uma interface fluente (cadeamento de método), que fornece várias vantagens:

  • Readingability: Código lê-se como linguagem natural
  • Flexibilidade: Fácil de adicionar ou remover as etapas de configuração
  • Imutabilidade: O objeto final pode ser imutável enquanto o construtor é mutável
  • Validação: A lógica de construção pode validar o objeto antes da criação
  • Reusabilidade: Construtores podem ser reutilizados para criar vários objetos similares

Padrão adaptador: Fazendo interfaces incompatíveis trabalhar juntos

O padrão Adaptador permite que objetos com interfaces incompatíveis colaborem. Este padrão estrutural atua como uma ponte entre duas interfaces incompatíveis, permitindo que as classes trabalhem juntas que não poderiam, de outra forma, devido a interfaces incompatíveis.

O Método Adaptador é um padrão de design estrutural que permite que você faça duas interfaces incompatíveis trabalharem juntas criando uma ponte entre elas. Isto é particularmente valioso ao integrar bibliotecas de terceiros, código legado ou APIs externas em sua aplicação.

Implementação do Adaptador do Mundo Real

Considere um cenário em que você precisa integrar múltiplos gateways de pagamento com diferentes interfaces:

from abc import ABC, abstractmethod

# Target interface that our application expects
class PaymentProcessor(ABC):
 @abstractmethod
 def process_payment(self, amount: float, currency: str) -> dict:
 pass

# Adaptee 1: Stripe API (incompatible interface)
class StripeAPI:
 def create_charge(self, amount_cents: int, currency_code: str, source: str):
 return {
 'id': 'ch_stripe_123',
 'amount': amount_cents,
 'currency': currency_code,
 'status': 'succeeded'
 }

# Adaptee 2: PayPal API (different incompatible interface)
class PayPalAPI:
 def make_payment(self, total: float, currency_type: str, account: str):
 return {
 'transaction_id': 'pp_456',
 'total_amount': total,
 'currency': currency_type,
 'state': 'approved'
 }

# Adaptee 3: Square API (yet another incompatible interface)
class SquareAPI:
 def charge_card(self, money_amount: dict, card_token: str):
 return {
 'payment_id': 'sq_789',
 'amount_money': money_amount,
 'status': 'COMPLETED'
 }

# Adapter for Stripe
class StripeAdapter(PaymentProcessor):
 def __init__(self, stripe_api: StripeAPI):
 self.stripe = stripe_api

 def process_payment(self, amount: float, currency: str) -> dict:
 # Convert dollars to cents for Stripe
 amount_cents = int(amount * 100)

 # Call Stripe API with adapted parameters
 result = self.stripe.create_charge(
 amount_cents=amount_cents,
 currency_code=currency.upper(),
 source='tok_visa'
 )

 # Adapt the response to our standard format
 return {
 'success': result['status'] == 'succeeded',
 'transaction_id': result['id'],
 'amount': amount,
 'currency': currency,
 'provider': 'Stripe'
 }

# Adapter for PayPal
class PayPalAdapter(PaymentProcessor):
 def __init__(self, paypal_api: PayPalAPI):
 self.paypal = paypal_api

 def process_payment(self, amount: float, currency: str) -> dict:
 result = self.paypal.make_payment(
 total=amount,
 currency_type=currency.upper(),
 account='[email protected]'
 )

 return {
 'success': result['state'] == 'approved',
 'transaction_id': result['transaction_id'],
 'amount': amount,
 'currency': currency,
 'provider': 'PayPal'
 }

# Adapter for Square
class SquareAdapter(PaymentProcessor):
 def __init__(self, square_api: SquareAPI):
 self.square = square_api

 def process_payment(self, amount: float, currency: str) -> dict:
 money_amount = {
 'amount': int(amount * 100),
 'currency': currency.upper()
 }

 result = self.square.charge_card(
 money_amount=money_amount,
 card_token='cnon:card-token'
 )

 return {
 'success': result['status'] == 'COMPLETED',
 'transaction_id': result['payment_id'],
 'amount': amount,
 'currency': currency,
 'provider': 'Square'
 }

# Client code that works with the unified interface
class PaymentService:
 def __init__(self, processor: PaymentProcessor):
 self.processor = processor

 def charge_customer(self, amount: float, currency: str = 'USD'):
 result = self.processor.process_payment(amount, currency)

 if result['success']:
 print(f"✓ Payment successful via {result['provider']}")
 print(f" Transaction ID: {result['transaction_id']}")
 print(f" Amount: {result['amount']} {result['currency']}")
 else:
 print(f"✗ Payment failed")

 return result

# Usage - all payment gateways work through the same interface
stripe_processor = StripeAdapter(StripeAPI())
paypal_processor = PayPalAdapter(PayPalAPI())
square_processor = SquareAdapter(SquareAPI())

# Process payments using different providers
service = PaymentService(stripe_processor)
service.charge_customer(99.99)

service = PaymentService(paypal_processor)
service.charge_customer(149.99)

service = PaymentService(square_processor)
service.charge_customer(199.99)

Padrão do Método do Modelo: Definindo Esqueletos do Algoritmo

O Método de Modelo define o esqueleto de um algoritmo na superclasse, mas permite que as subclasses sobreponham as etapas específicas do algoritmo sem alterar a sua estrutura. Este padrão comportamental é excelente para executar um processo consistente, permitindo a personalização em pontos específicos.

Aplicação do Método do Modelo

from abc import ABC, abstractmethod
import time

class DataProcessor(ABC):
 """Template class defining the data processing algorithm"""

 def process(self, data):
 """Template method defining the algorithm structure"""
 print("Starting data processing pipeline...")

 # Step 1: Validate
 if not self.validate(data):
 raise ValueError("Data validation failed")

 # Step 2: Extract
 extracted = self.extract(data)

 # Step 3: Transform
 transformed = self.transform(extracted)

 # Step 4: Load
 result = self.load(transformed)

 # Step 5: Cleanup (optional hook)
 self.cleanup()

 print("Data processing completed successfully")
 return result

 @abstractmethod
 def validate(self, data) -> bool:
 """Validate input data - must be implemented by subclasses"""
 pass

 @abstractmethod
 def extract(self, data):
 """Extract relevant data - must be implemented by subclasses"""
 pass

 @abstractmethod
 def transform(self, data):
 """Transform data - must be implemented by subclasses"""
 pass

 @abstractmethod
 def load(self, data):
 """Load processed data - must be implemented by subclasses"""
 pass

 def cleanup(self):
 """Optional hook method - can be overridden by subclasses"""
 print("Performing default cleanup...")

class CSVDataProcessor(DataProcessor):
 def validate(self, data) -> bool:
 print("Validating CSV data...")
 return isinstance(data, str) and len(data) > 0

 def extract(self, data):
 print("Extracting CSV data...")
 lines = data.strip().split('n')
 headers = lines[0].split(',')
 rows = [line.split(',') for line in lines[1:]]
 return {'headers': headers, 'rows': rows}

 def transform(self, data):
 print("Transforming CSV data...")
 headers = data['headers']
 rows = data['rows']
 return [dict(zip(headers, row)) for row in rows]

 def load(self, data):
 print(f"Loading {len(data)} CSV records...")
 return data

 def cleanup(self):
 print("Cleaning up CSV processing resources...")

class JSONDataProcessor(DataProcessor):
 def validate(self, data) -> bool:
 print("Validating JSON data...")
 import json
 try:
 json.loads(data)
 return True
 except:
 return False

 def extract(self, data):
 print("Extracting JSON data...")
 import json
 return json.loads(data)

 def transform(self, data):
 print("Transforming JSON data...")
 if isinstance(data, list):
 return [self._flatten_dict(item) for item in data]
 return [self._flatten_dict(data)]

 def _flatten_dict(self, d, parent_key='', sep='_'):
 items = []
 for k, v in d.items():
 new_key = f"{parent_key}{sep}{k}" if parent_key else k
 if isinstance(v, dict):
 items.extend(self._flatten_dict(v, new_key, sep=sep).items())
 else:
 items.append((new_key, v))
 return dict(items)

 def load(self, data):
 print(f"Loading {len(data)} JSON records...")
 return data

class XMLDataProcessor(DataProcessor):
 def validate(self, data) -> bool:
 print("Validating XML data...")
 return data.strip().startswith('')

 def extract(self, data):
 print("Extracting XML data...")
 # Simplified XML parsing
 return {'xml_content': data}

 def transform(self, data):
 print("Transforming XML data...")
 return {'processed_xml': data['xml_content']}

 def load(self, data):
 print("Loading XML data...")
 return data

 def cleanup(self):
 print("Cleaning up XML processing resources...")
 print("Closing XML parser...")

# Usage
csv_data = """name,age,city
John,30,New York
Jane,25,Los Angeles
Bob,35,Chicago"""

json_data = '''[
 {"name": "John", "age": 30, "address": {"city": "New York"}},
 {"name": "Jane", "age": 25, "address": {"city": "Los Angeles"}}
]'''

xml_data = "John30"

# Process CSV
csv_processor = CSVDataProcessor()
csv_result = csv_processor.process(csv_data)
print(f"CSV Result: {csv_result}n")

# Process JSON
json_processor = JSONDataProcessor()
json_result = json_processor.process(json_data)
print(f"JSON Result: {json_result}n")

# Process XML
xml_processor = XMLDataProcessor()
xml_result = xml_processor.process(xml_data)
print(f"XML Result: {xml_result}")

Quando usar padrões de projeto

Saber quando usar padrões de design é crucial para o design de software eficaz, particularmente quando você encontra problemas de design recorrentes que têm soluções bem estabelecidas, já que padrões de design fornecem abordagens testadas e comprovadas para desafios de design de software comuns.

Casos de Uso Apropriados

Use padrões de design para promover a reutilização, flexibilidade e manutenção de código, pois ajudam na estruturação de código de uma forma que facilita a modificação e extensão à medida que os requisitos evoluem. Considere padrões de implementação quando:

  • Você enfrenta um problema que corresponde a um padrão conhecido de intenção
  • O padrão oferece benefícios claros sobre uma solução mais simples
  • A sua equipa compreende o padrão e pode mantê-lo.
  • A complexidade adicionada justifica-se por uma maior flexibilidade ou manutenção
  • Você quer melhorar a comunicação entre os membros da equipe

Quando não usar padrões

O melhor código é o código mais simples que resolve corretamente o problema – às vezes é um padrão de design bem colocado, mas muitas vezes é apenas uma função e um dict. Evite padrões quando:

  • Uma solução mais simples funcionaria igualmente
  • Você está aplicando padrões para o bem de usar padrões
  • O padrão adiciona complexidade desnecessária ao código simples
  • Sua equipe não está familiarizado com o padrão e falta documentação
  • O problema não corresponde à intenção do padrão

Cada padrão tem seus próprios trade-offs, e você precisa prestar mais atenção ao porquê de você estar escolhendo um determinado padrão do que a forma de implementá-lo. A decisão de usar um padrão deve ser impulsionada pelo problema em questão, não pelo desejo de demonstrar conhecimento de padrões.

Anti- Parâmetros a Evitar em Python

Nem todos os padrões fazem sentido no ecossistema do Python. Os módulos Python já são singletons – cada módulo é importado apenas uma vez, então classes de singleton explícitas adicionam complexidade desnecessária, com melhores alternativas sendo variáveis de nível de módulo ou injeção de dependência.

Padrões que não se encaixam bem em Python

  • God Object: Centraliza muita lógica em uma única classe, torna o código mais difícil de testar e manter, com a melhor alternativa é dividir a funcionalidade em classes menores e coesas
  • Hierarquias de Herança Profunda: Árvores de herança profunda tornam o código frágil, por isso preferem composição e delegação
  • Abstrações desnecessárias: A digitação de patos do Python muitas vezes elimina a necessidade de hierarquias complexas de interface

Considerações de Padrão Python Modernas

No Python moderno (3.8+), prefira Protocolo para subtipagem estrutural, pois não requer herança explícita, com classes satisfazendo um Protocolo apenas por ter os métodos certos, enquanto usa o ABC quando deseja impor herança e fornecer implementações padrão.

Usando protocolos para digitação de patos

from typing import Protocol

class Drawable(Protocol):
 def draw(self) -> str:
 ...

class Circle:
 def draw(self) -> str:
 return "Drawing a circle"

class Square:
 def draw(self) -> str:
 return "Drawing a square"

def render(shape: Drawable) -> None:
 print(shape.draw())

# Both Circle and Square satisfy the Drawable protocol
# without explicit inheritance
render(Circle())
render(Square())

Padrões de design em Aplicações Python de Produção

Em 2026, Python está sentado na intersecção de IA e machine learning, desenvolvimento de produtos web escaláveis e engenharia de dados empresariais, com sua dominância refletindo uma vantagem estrutural que as equipes de engenharia descobriram anos atrás: Python permite que você se mova mais rápido, se integre mais facilmente e construa sistemas que são mantendíveis por equipes, não apenas pelo autor original.

Padrões Específicos de Quadro

Django continua a ser o framework Python mais completo para equipes que constroem produtos com acesso multiusuário, modelos de dados complexos, interfaces de administração e sistemas de autenticação. Django usa extensivamente padrões como:

  • Modelo de Template de Vista (MVT): Variação de MVC por Django
  • Record ativo: Modelos ORM de Django
  • Método de Template: Vistas com base em classes
  • Cadeia de middleware: Processamento de pedido/resposta

FastAPI aproveita os modernos recursos e padrões Python:

  • Injecção de dependência: Sistema DI integrado
  • Padrão de decoradores: Decoradores de rotas
  • [[FLT: 0]] Padrão de Estratégia: Autenticação plugável
  • Padrão de Fábrica: Criação de modelos de resposta

Padrões Arquitetônicos

Em 2026, o consenso entre as equipes de engenharia experientes Python é mais claro: comece com um monólito bem estruturado, decomponha-se em serviços quando limites específicos emergem do uso real, pois um monólito não é um modo de falha.

O Instagram passou por um monolito Django bem depois de 100 milhões de usuários antes de decompor funções específicas de alta carga em serviços, com a chave sendo construir o monolito com decomposição de serviço em mente desde o início através de limites de módulo claros, acoplamento de módulo transversal mínimo e padrões de comunicação orientados para eventos.

Teste de padrões de projeto

Os padrões de design devem tornar o código mais testável, não menos. Aqui estão as estratégias para testar implementações de padrões:

Padrão de estratégia de teste

import unittest
from unittest.mock import Mock

class TestPaymentStrategies(unittest.TestCase):
 def test_credit_card_payment(self):
 strategy = CreditCardPayment("1234567890123456", "123", "12/25")
 result = strategy.pay(100.00)
 self.assertIn("Credit Card", result)
 self.assertIn("100.00", result)

 def test_paypal_payment(self):
 strategy = PayPalPayment("[email protected]")
 result = strategy.pay(50.00)
 self.assertIn("PayPal", result)
 self.assertIn("[email protected]", result)

 def test_shopping_cart_with_different_strategies(self):
 cart = ShoppingCart()
 cart.add_item("Item 1", 50.00)

 # Test with credit card
 cart.set_payment_strategy(CreditCardPayment("1234", "123", "12/25"))
 result1 = cart.checkout()

 # Test with PayPal
 cart.set_payment_strategy(PayPalPayment("[email protected]"))
 result2 = cart.checkout()

 self.assertIsNotNone(result1)
 self.assertIsNotNone(result2)

Padrão de Observador de Testes

class TestObserverPattern(unittest.TestCase):
 def test_observer_notification(self):
 stock = StockPrice("AAPL", 150.00)
 observer = Mock(spec=Observer)

 stock.attach(observer)
 stock.price = 155.00

 observer.update.assert_called_once_with(stock)

 def test_multiple_observers(self):
 stock = StockPrice("GOOGL", 2800.00)
 observer1 = Mock(spec=Observer)
 observer2 = Mock(spec=Observer)

 stock.attach(observer1)
 stock.attach(observer2)
 stock.price = 2850.00

 observer1.update.assert_called_once()
 observer2.update.assert_called_once()

 def test_detach_observer(self):
 stock = StockPrice("MSFT", 300.00)
 observer = Mock(spec=Observer)

 stock.attach(observer)
 stock.detach(observer)
 stock.price = 310.00

 observer.update.assert_not_called()

Considerações sobre o desempenho

Embora os padrões de projeto melhorem a organização e manutenção do código, eles podem introduzir sobrecarga de desempenho se não forem implementados cuidadosamente.

Inicialização Preguiçosa

A instância é criada somente quando o método get instance() é chamado pela primeira vez, garantindo que os recursos são alocados apenas quando necessário. Isto é particularmente importante para objetos com uso intensivo de recursos:

class DatabaseConnection:
 _instance = None
 _initialized = False

 def __new__(cls):
 if cls._instance is None:
 cls._instance = super().__new__(cls)
 return cls._instance

 def __init__(self):
 if not DatabaseConnection._initialized:
 self._connect()
 DatabaseConnection._initialized = True

 def _connect(self):
 # Expensive connection operation
 print("Establishing database connection...")
 self.connection = "Connected"

Resultados do Decorador de Cache

from functools import lru_cache

@lru_cache(maxsize=128)
def expensive_computation(n):
 """Cached using built-in LRU cache"""
 print(f"Computing for {n}...")
 return sum(range(n))

# First call computes
result1 = expensive_computation(1000)

# Second call returns cached result
result2 = expensive_computation(1000)

Melhores Práticas para Padrões de Design em Python

Os padrões de design devem simplificar, não complicar, pois os padrões de design Python não são sobre copiar diagramas de livros didáticos, eles são sobre resolver problemas reais de forma elegante. Siga estas diretrizes para implementação de padrões eficazes:

1. Composição Favor sobre Herança

A natureza dinâmica do Python torna a composição particularmente poderosa. Em vez de hierarquias de herança profunda, compõe objetos de componentes menores e focados:

# Instead of inheritance
class EmailNotifier:
 def send(self, message):
 print(f"Sending email: {message}")

class SMSNotifier:
 def send(self, message):
 print(f"Sending SMS: {message}")

# Use composition
class NotificationService:
 def __init__(self):
 self.notifiers = []

 def add_notifier(self, notifier):
 self.notifiers.append(notifier)

 def notify(self, message):
 for notifier in self.notifiers:
 notifier.send(message)

# Usage
service = NotificationService()
service.add_notifier(EmailNotifier())
service.add_notifier(SMSNotifier())
service.notify("Important update!")

2. Use Dica do Tipo para a clareza

Dica de tipo torna as implementações de padrão mais explícitas e permite melhor suporte ao IDE:

from typing import Protocol, List
from abc import abstractmethod

class PaymentMethod(Protocol):
 def process(self, amount: float) -> bool:
 ...

class PaymentProcessor:
 def __init__(self, methods: List[PaymentMethod]):
 self.methods = methods

 def charge(self, amount: float) -> bool:
 for method in self.methods:
 if method.process(amount):
 return True
 return False

3. Uso do Padrão de Documento

Documente sempre qual padrão você está usando e por quê:

class DataExporter:
 """
 Uses the Strategy pattern to support multiple export formats.

 This allows adding new export formats without modifying existing code,
 following the Open/Closed Principle.

 Example:
 exporter = DataExporter(JSONExportStrategy())
 exporter.export(data)
 """

 def __init__(self, strategy: ExportStrategy):
 self._strategy = strategy

 def export(self, data):
 return self._strategy.export(data)

4. Mantenha-o simples

Não exagere na engenharia. Inicie simples e refator para padrões quando a complexidade o justificar:

# Simple is often better
def calculate_discount(price, customer_type):
 discounts = {
 'regular': 0,
 'premium': 0.10,
 'vip': 0.20
 }
 return price * (1 - discounts.get(customer_type, 0))

# Only introduce Strategy pattern when you need:
# - Runtime strategy switching
# - Complex discount logic
# - Multiple discount calculation methods

Recursos para Aprender Padrões de Design

Para aprofundar sua compreensão de padrões de design em Python, explore esses recursos valiosos:

Conclusão

Os padrões de design são ferramentas poderosas em um kit de ferramentas de um engenheiro Python, mas devem ser aplicados criteriosamente. Os padrões de estratégia, fábrica e observador muitas vezes aparecem juntos em sistemas Python bem arquitetados, com a estratégia focada em selecionar e trocar algoritmos, a Fábrica concentrada em como os objetos são criados e o Observador abordando a comunicação, com a compreensão dessas distinções ajudando você a escolher o padrão certo com base em se o seu desafio é sobre comportamento, criação ou comunicação.

A chave para usar com sucesso padrões de design em Python é entender tanto os padrões em si como as características únicas de Python. A digitação dinâmica, funções de primeira classe e biblioteca padrão poderosa muitas vezes permitem implementações mais simples do que em linguagens com digitação estática. Sempre priorize a clareza de código e manutenção sobre a pureza de padrões.

Lembre-se que os padrões são meios para um fim, não termina em si mesmos. O objetivo é escrever um código que seja fácil de entender, testar e modificar. Quando um padrão ajuda a atingir esse objetivo, use-o. Quando uma solução mais simples for suficiente, abrace a simplicidade. À medida que você ganha experiência, você desenvolverá uma intuição para quando os padrões adicionam valor e quando eles adicionam complexidade desnecessária.

Ao dominar padrões de design e entender quando aplicá-los, você estará mais bem equipado para construir aplicativos Python robustos e escaláveis que suportam o teste do tempo e os requisitos em evolução.