Injeção de Dependência: Uma Abordagem Python

Padrões de DI 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 você 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 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 para protocolos, classes base abstratas e tipagem pato. A flexibilidade do Python significa que você pode implementar padrões de 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 que você escreva 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 trocar dependências reais por duplos de teste.

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

Desacoplamento: 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 diferentes implementações para diferentes ambientes (desenvolvimento, teste, produção) sem alterar sua lógica de negócios. Isso é especialmente útil ao implantar aplicações Python em diferentes plataformas, sejam AWS Lambda ou servidores tradicionais.

Injeção por Construtor: O jeito Python

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

Exemplo Básico

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

from typing import Protocol
from abc import ABC, abstractmethod

# Definindo 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)

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

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 Base Abstratas

Um dos princípios principais 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 Base Abstratas (ABCs) (tipagem nominal).

Protocolos (Python 3.8+)

Protocolos usam tipagem estrutural — se um objeto tiver 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 esse 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 Base Abstratas

ABCs usam tipagem nominal — classes devem herdar explicitamente da 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 Real: Abstração de Banco de Dados

Ao trabalhar com bancos de dados em aplicações Python, você frequentemente precisa abstrair operações de banco de dados. Aqui está 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

Esse padrão permite trocar implementações de banco de dados (PostgreSQL, SQLite, MongoDB) sem alterar seu código de repositório.

O Padrão Raiz de Composição

O Padrão Raiz de Composição é onde você monta todas as suas dependências no ponto de entrada da aplicação (normalmente main.py ou sua fábrica de aplicação). Isso centraliza a configuração de dependências e torna o gráfico de dependências explícito.

def create_app() -> FastAPI:
    app = FastAPI()
    
    # Inicializando dependências de infraestrutura
    db = init_database()
    logger = init_logger()
    
    # Inicializando repositórios
    user_repo = UserRepository(db)
    order_repo = OrderRepository(db)
    
    # Inicializando 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)
    
    # Inicializando manipuladores HTTP
    user_handler = UserHandler(user_svc)
    order_handler = OrderHandler(order_svc)
    
    # Conectando rotas
    app.include_router(user_handler.router)
    app.include_router(order_handler.router)
    
    return app

Essa abordagem torna claro como sua aplicação está estruturada e de onde vêm as dependências. É particularmente valiosa 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 de DI que podem ajudar:

Dependency Injector

Dependency Injector é um framework popular que fornece uma abordagem baseada em contêiner 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 da Google, com foco 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 gráfico de dependência é complexo com muitos componentes interdependentes
  • Você tem várias implementações da mesma interface que precisam ser selecionadas com base na configuração
  • Você deseja resolução automática de dependências
  • Você está construindo uma aplicação grande onde a fiação manual se torna propensa a erros

Mantenha a DI manual quando:

  • Sua aplicação é pequena a média
  • O gráfico de dependência é simples e fácil de seguir
  • Você deseja manter as dependências mínimas e explícitas
  • Você prefere código explícito em vez de magia de framework

Testando com Injeção de Dependência

Um dos principais benefícios da injeção de dependência é a melhoria na testabilidade. Aqui está como a DI torna o teste mais fácil:

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"

Esse teste executa rapidamente, não requer um banco de dados e testa sua lógica de negócios em isolamento. 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 Boas Práticas

1. Use a Segregação de Interfaces

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

# Bom: O 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("user repository cannot be None")
        self.repo = repo

3. Use Dicas de Tipo

Dicas de tipo tornam dependências explícitas e ajudam com suporte do IDE e verificação de tipo estática:

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 a Injeção Excessiva

Não injete dependências que são realmente detalhes de implementação interna. Se um componente cria e gerencia seus próprios objetos auxiliares, está tudo bem:

# Bom: O helper 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 as dependências são necessárias e quaisquer restrições:

class UserService:
    """UserService lida com a 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

FastAPI tem suporte integrado para injeção de dependência por meio de seu 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 de FastAPI lida automaticamente com o gráfico de dependência, tornando fácil construir 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 classes de dados
  • Funções ou utilitários internos
  • Scripts de uso único ou utilitários pequenos
  • Quando a instânciação direta for mais clara e simples

Exemplo de quando NÃO usar DI:

# Classe de dados simples - não é necessário usar DI
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

# Utilitário simples - não é necessário usar DI
def format_currency(amount: float) -> str:
    return f"${amount:.2f}"

Integração com o Ecossistema Python

A injeção de dependência funciona de forma perfeita com outros padrões e ferramentas Python. Ao construir aplicações que usam pacotes Python ou frameworks de testes unitários, 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"Gerado relatório {report_id}")
        return pdf

Isso permite trocar implementações ou usar 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 é um pilar da escrita de código Python mantível e testável. Ao seguir os padrões apresentados neste artigo — injeção por construtor, design baseado em protocolos e o padrão raiz de composição — você criará aplicações mais fáceis de entender, testar e modificar.

Comece com a injeção manual por construtor para aplicações pequenas a médias, e considere frameworks como dependency-injector ou injector conforme seu gráfico de dependência crescer. Lembre-se de que o objetivo é clareza e testabilidade, não complexidade por si só.

Para mais recursos de desenvolvimento em Python, consulte nosso Folha de Dicas de Python para referência rápida sobre sintaxe Python e padrões comuns.

Recursos Externos