Injeção de Dependência: A Maneira Python

Padrões de Injeção de Dependência em Python para código limpo e testável

Conteúdo da página

Injeção de dependência (DI) é um padrão de design fundamental que promove código limpo, testável e mantível em aplicações Python.

Seja construindo APIs REST com FastAPI, implementando testes unitários, ou trabalhando com funções AWS Lambda, entender a injeção de dependência melhorará significativamente a qualidade do seu código.

python packages

O que é Injeção de Dependência?

A injeção de dependência é um padrão de design onde os componentes recebem suas dependências de fontes externas em vez de criá-las internamente. Essa abordagem desacopla os componentes, tornando seu código mais modular, testável e mantível.

Em Python, a injeção de dependência é particularmente poderosa devido à natureza dinâmica da linguagem e ao suporte a protocolos, classes abstratas base e tipagem por pato. A flexibilidade do Python significa que você pode implementar padrões DI sem frameworks pesados, embora frameworks estejam disponíveis quando necessários.

Por que Usar Injeção de Dependência em Python?

Melhor Testabilidade: Ao injetar dependências, você pode facilmente substituir implementações reais por mocks ou duplos de teste. Isso permite escrever testes unitários que são rápidos, isolados e não requerem serviços externos como bancos de dados ou APIs. Ao escrever testes unitários abrangentes, a injeção de dependência torna trivial substituir dependências reais por duplos de teste.

Melhor Mantibilidade: As dependências tornam-se explícitas no seu código. Quando você olha para um construtor, você vê imediatamente o que um componente requer. Isso torna a base de código mais fácil de entender e modificar.

Acoplamento Fraco: Os componentes dependem de abstrações (protocolos ou ABCs) em vez de implementações concretas. Isso significa que você pode mudar implementações sem afetar o código dependente.

Flexibilidade: Você pode configurar implementações diferentes para ambientes diferentes (desenvolvimento, teste, produção) sem alterar a lógica de negócios. Isso é especialmente útil ao implantar aplicações Python em diferentes plataformas, seja em AWS Lambda ou servidores tradicionais.

Injeção de Construtor: A Maneira Python

A maneira mais comum e idiomática de implementar injeção de dependência em Python é através da injeção de construtor — aceitando dependências como parâmetros no método __init__.

Exemplo Básico

Aqui está um exemplo simples demonstrando injeção de construtor:

from typing import Protocol
from abc import ABC, abstractmethod

# Defina um protocolo para o repositório
class UserRepository(Protocol):
    def find_by_id(self, user_id: int) -> 'User | None':
        ...
    
    def save(self, user: 'User') -> 'User':
        ...

# Serviço depende do protocolo do repositório
class UserService:
    def __init__(self, repo: UserRepository):
        self.repo = repo
    
    def get_user(self, user_id: int) -> 'User | None':
        return self.repo.find_by_id(user_id)

Este padrão torna claro que UserService requer um UserRepository. Você não pode criar um UserService sem fornecer um repositório, o que previne erros de tempo de execução devido a dependências faltantes.

Múltiplas Dependências

Quando um componente tem múltiplas dependências, basta adicioná-las como parâmetros do construtor:

class EmailService(Protocol):
    def send(self, to: str, subject: str, body: str) -> None:
        ...

class Logger(Protocol):
    def info(self, msg: str) -> None:
        ...
    
    def error(self, msg: str, err: Exception) -> None:
        ...

class OrderService:
    def __init__(
        self,
        repo: OrderRepository,
        email_svc: EmailService,
        logger: Logger,
        payment_svc: PaymentService,
    ):
        self.repo = repo
        self.email_svc = email_svc
        self.logger = logger
        self.payment_svc = payment_svc

Usando Protocolos e Classes Abstratas Base

Um dos princípios-chave ao implementar injeção de dependência é o Princípio da Inversão de Dependência (DIP): módulos de alto nível não devem depender de módulos de baixo nível; ambos devem depender de abstrações.

Em Python, você pode definir abstrações usando Protocolos (tipagem estrutural) ou Classes Abstratas Base (ABCs) (tipagem nominal).

Protocolos (Python 3.8+)

Protocolos usam tipagem estrutural — se um objeto tem os métodos necessários, ele satisfaz o protocolo:

from typing import Protocol

class PaymentProcessor(Protocol):
    def process_payment(self, amount: float) -> bool:
        ...

# Qualquer classe com o método process_payment satisfaz este protocolo
class CreditCardProcessor:
    def process_payment(self, amount: float) -> bool:
        # Lógica de cartão de crédito
        return True

class PayPalProcessor:
    def process_payment(self, amount: float) -> bool:
        # Lógica do PayPal
        return True

# Serviço aceita qualquer PaymentProcessor
class OrderService:
    def __init__(self, payment_processor: PaymentProcessor):
        self.payment_processor = payment_processor

Classes Abstratas Base

ABCs usam tipagem nominal — as classes devem herdar explicitamente do ABC:

from abc import ABC, abstractmethod

class PaymentProcessor(ABC):
    @abstractmethod
    def process_payment(self, amount: float) -> bool:
        pass

class CreditCardProcessor(PaymentProcessor):
    def process_payment(self, amount: float) -> bool:
        return True

Quando usar Protocolos vs ABCs: Use Protocolos quando quiser tipagem estrutural e flexibilidade. Use ABCs quando precisar impor hierarquias de herança ou fornecer implementações padrão.

Exemplo do Mundo Real: Abstração de Banco de Dados

Ao trabalhar com bancos de dados em aplicações Python, você frequentemente precisará abstrair operações de banco de dados. Veja como a injeção de dependência ajuda:

from typing import Protocol, Optional
from contextlib import contextmanager

class Database(Protocol):
    @contextmanager
    def transaction(self):
        ...
    
    def execute(self, query: str, params: dict) -> None:
        ...
    
    def fetch_one(self, query: str, params: dict) -> Optional[dict]:
        ...

# Repositório depende da abstração
class UserRepository:
    def __init__(self, db: Database):
        self.db = db
    
    def find_by_id(self, user_id: int) -> Optional['User']:
        result = self.db.fetch_one(
            "SELECT * FROM users WHERE id = :id",
            {"id": user_id}
        )
        if result:
            return User(**result)
        return None

Este padrão permite que você alterne implementações de banco de dados (PostgreSQL, SQLite, MongoDB) sem alterar seu código de repositório.

O Padrão Composition Root (Raiz de Composição)

A Composition Root é onde você monta todas as suas dependências no ponto de entrada da aplicação (tipicamente main.py ou sua fábrica de aplicação). Isso centraliza a configuração de dependências e torna o grafo de dependências explícito.

def create_app() -> FastAPI:
    app = FastAPI()
    
    # Inicialize dependências de infraestrutura
    db = init_database()
    logger = init_logger()
    
    # Inicialize repositórios
    user_repo = UserRepository(db)
    order_repo = OrderRepository(db)
    
    # Inicialize serviços com dependências
    email_svc = EmailService(logger)
    payment_svc = PaymentService(logger)
    user_svc = UserService(user_repo, logger)
    order_svc = OrderService(order_repo, email_svc, logger, payment_svc)
    
    # Inicialize manipuladores HTTP
    user_handler = UserHandler(user_svc)
    order_handler = OrderHandler(order_svc)
    
    # Conecte rotas
    app.include_router(user_handler.router)
    app.include_router(order_handler.router)
    
    return app

Essa abordagem torna claro como sua aplicação é estruturada e de onde vêm as dependências. É particularmente valioso ao construir aplicações seguindo princípios de arquitetura limpa, onde você precisa coordenar múltiplas camadas de dependências.

Frameworks de Injeção de Dependência

Para aplicações maiores com grafos de dependência complexos, gerenciar dependências manualmente pode se tornar trabalhoso. Python tem vários frameworks DI que podem ajudar:

Dependency Injector

Dependency Injector é um framework popular que fornece uma abordagem baseada em container para injeção de dependência.

Instalação:

pip install dependency-injector

Exemplo:

from dependency_injector import containers, providers
from dependency_injector.wiring import inject, Provide

class Container(containers.DeclarativeContainer):
    # Configuração
    config = providers.Configuration()
    
    # Banco de dados
    db = providers.Singleton(
        Database,
        connection_string=config.database.url
    )
    
    # Repositórios
    user_repository = providers.Factory(
        UserRepository,
        db=db
    )
    
    # Serviços
    user_service = providers.Factory(
        UserService,
        repo=user_repository
    )

# Uso
container = Container()
container.config.database.url.from_env("DATABASE_URL")
user_service = container.user_service()

Injector

Injector é uma biblioteca leve inspirada no Guice do Google, focando em simplicidade.

Instalação:

pip install injector

Exemplo:

from injector import Injector, inject, Module, provider

class DatabaseModule(Module):
    @provider
    def provide_db(self) -> Database:
        return Database(connection_string="...")

class UserModule(Module):
    @inject
    def __init__(self, repo: UserRepository):
        self.repo = repo

injector = Injector([DatabaseModule()])
user_service = injector.get(UserService)

Quando Usar Frameworks

Use um framework quando:

  • Seu grafo de dependência for complexo com muitos componentes interdependentes
  • Você tiver múltiplas implementações da mesma interface que precisam ser selecionadas com base na configuração
  • Você quiser resolução de dependência automática
  • Você estiver construindo uma aplicação grande onde o cabeamento manual se torna propenso a erros

Mantenha a DI manual quando:

  • Sua aplicação for de pequeno a médio porte
  • O grafo de dependência for simples e fácil de seguir
  • Você quiser manter as dependências mínimas e explícitas
  • Você preferir código explícito a “mágica” de frameworks

Testando com Injeção de Dependência

Um dos principais benefícios da injeção de dependência é a testabilidade melhorada. Veja como a DI torna os testes mais fáceis:

Exemplo de Teste Unitário

from unittest.mock import Mock
import pytest

# Implementação Mock para testes
class MockUserRepository:
    def __init__(self):
        self.users = {}
        self.error = None
    
    def find_by_id(self, user_id: int) -> Optional['User']:
        if self.error:
            raise self.error
        return self.users.get(user_id)
    
    def save(self, user: 'User') -> 'User':
        if self.error:
            raise self.error
        self.users[user.id] = user
        return user

# Teste usando o mock
def test_user_service_get_user():
    mock_repo = MockUserRepository()
    mock_repo.users[1] = User(id=1, name="John", email="john@example.com")
    
    service = UserService(mock_repo)
    
    user = service.get_user(1)
    assert user is not None
    assert user.name == "John"

Este teste roda rapidamente, não requer um banco de dados e testa sua lógica de negócios de forma isolada. Ao trabalhar com testes unitários em Python, a injeção de dependência torna fácil criar duplos de teste e verificar interações.

Usando Fixtures do pytest

Fixtures do pytest funcionam excelente com injeção de dependência:

@pytest.fixture
def mock_user_repository():
    return MockUserRepository()

@pytest.fixture
def user_service(mock_user_repository):
    return UserService(mock_user_repository)

def test_user_service_get_user(user_service, mock_user_repository):
    user = User(id=1, name="John", email="john@example.com")
    mock_user_repository.users[1] = user
    
    result = user_service.get_user(1)
    assert result.name == "John"

Padrões Comuns e Melhores Práticas

1. Use Segregação de Interface

Mantenha protocolos e interfaces pequenos e focados no que o cliente realmente precisa:

# Bom: Cliente só precisa ler usuários
class UserReader(Protocol):
    def find_by_id(self, user_id: int) -> Optional['User']:
        ...
    
    def find_by_email(self, email: str) -> Optional['User']:
        ...

# Interface separada para escrita
class UserWriter(Protocol):
    def save(self, user: 'User') -> 'User':
        ...
    
    def delete(self, user_id: int) -> bool:
        ...

2. Valide Dependências nos Construtores

Construtores devem validar dependências e levantar erros claros se a inicialização falhar:

class UserService:
    def __init__(self, repo: UserRepository):
        if repo is None:
            raise ValueError("repositório de usuário não pode ser None")
        self.repo = repo

3. Use Type Hints (Dicas de Tipo)

Type hints tornam as dependências explícitas e ajudam com suporte de IDE e verificação de tipo estático:

from typing import Protocol, Optional

class UserService:
    def __init__(
        self,
        repo: UserRepository,
        logger: Logger,
        email_service: EmailService,
    ) -> None:
        self.repo = repo
        self.logger = logger
        self.email_service = email_service

4. Evite Sobre-Injeção

Não injete dependências que são verdadeiros detalhes internos de implementação. Se um componente cria e gerencia seus próprios objetos auxiliares, isso é aceitável:

# Bom: Auxiliar interno não precisa de injeção
class UserService:
    def __init__(self, repo: UserRepository):
        self.repo = repo
        # Cache interno - não precisa de injeção
        self._cache: dict[int, User] = {}

5. Documente Dependências

Use docstrings para documentar por que dependências são necessárias e quaisquer restrições:

class UserService:
    """UserService lida com lógica de negócios relacionada a usuários.
    
    Args:
        repo: UserRepository para acesso a dados. Deve ser thread-safe
            se usado em contextos concorrentes.
        logger: Logger para rastreamento de erros e depuração.
    """
    def __init__(self, repo: UserRepository, logger: Logger):
        self.repo = repo
        self.logger = logger

Injeção de Dependência com FastAPI

O FastAPI tem suporte nativo para injeção de dependência através do mecanismo Depends:

from fastapi import FastAPI, Depends
from typing import Annotated

app = FastAPI()

def get_user_repository() -> UserRepository:
    db = get_database()
    return UserRepository(db)

def get_user_service(
    repo: Annotated[UserRepository, Depends(get_user_repository)]
) -> UserService:
    return UserService(repo)

@app.get("/users/{user_id}")
def get_user(
    user_id: int,
    service: Annotated[UserService, Depends(get_user_service)]
):
    user = service.get_user(user_id)
    if not user:
        raise HTTPException(status_code=404)
    return user

O sistema de injeção de dependência do FastAPI gerencia o grafo de dependências automaticamente, facilitando a construção de APIs limpas e mantíveis.

Quando NÃO Usar Injeção de Dependência

A injeção de dependência é uma ferramenta poderosa, mas nem sempre é necessária:

Pule a DI para:

  • Objetos de valor simples ou data classes
  • Funções auxiliares internas ou utilitários
  • Scripts únicos ou utilitários pequenos
  • Quando a instância direta for mais clara e simples

Exemplo de quando NÃO usar DI:

# Dataclass simples - sem necessidade de DI
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

# Utilitário simples - sem necessidade de DI
def format_currency(amount: float) -> str:
    return f"${amount:.2f}"

Integração com o Ecossistema Python

A injeção de dependência funciona perfeitamente com outros padrões e ferramentas do Python. Ao construir aplicações que usam pacotes Python ou frameworks de teste unitário, você pode injetar esses serviços em sua lógica de negócios:

class ReportService:
    def __init__(
        self,
        pdf_generator: PDFGenerator,
        repo: ReportRepository,
        logger: Logger,
    ):
        self.pdf_generator = pdf_generator
        self.repo = repo
        self.logger = logger
    
    def generate_report(self, report_id: int) -> bytes:
        report_data = self.repo.get_by_id(report_id)
        pdf = self.pdf_generator.generate(report_data)
        self.logger.info(f"Generated report {report_id}")
        return pdf

Isso permite que você alterne implementações ou use mocks durante os testes.

Injeção de Dependência Assíncrona

A sintaxe async/await do Python funciona bem com injeção de dependência:

from typing import Protocol
import asyncio

class AsyncUserRepository(Protocol):
    async def find_by_id(self, user_id: int) -> Optional['User']:
        ...
    
    async def save(self, user: 'User') -> 'User':
        ...

class AsyncUserService:
    def __init__(self, repo: AsyncUserRepository):
        self.repo = repo
    
    async def get_user(self, user_id: int) -> Optional['User']:
        return await self.repo.find_by_id(user_id)
    
    async def get_users_batch(self, user_ids: list[int]) -> list['User']:
        tasks = [self.repo.find_by_id(uid) for uid in user_ids]
        results = await asyncio.gather(*tasks)
        return [u for u in results if u is not None]

Conclusão

A injeção de dependência é uma pedra angular na escrita de código Python mantível e testável. Seguindo os padrões descritos neste artigo — injeção de construtor, design baseado em protocolos e o padrão Composition Root — você criará aplicações mais fáceis de entender, testar e modificar.

Comece com injeção de construtor manual para aplicações de pequeno a médio porte, e considere frameworks como dependency-injector ou injector conforme seu grafo de dependências cresce. Lembre-se que o objetivo é clareza e testabilidade, não complexidade por si só.

Para mais recursos de desenvolvimento Python, consulte nossa Lista de Atalhos Python para referência rápida de sintaxe Python e padrões comuns.

Recursos Externos

Assinar

Receba novos artigos sobre sistemas, infraestrutura e engenharia de IA.