Refatoração de Módulo de Configuração com Mixins em Python

Configuração monolítica

Quando um projeto Python cresce, é comum centralizar toda a configuração em uma única classe. Em pouco tempo, esse arquivo acumula centenas de linhas com endereços de serviços, credenciais e parâmetros de bibliotecas externas. Alterar ou localizar uma única chave se torna trabalhoso.

Uma forma de manter a API de acesso intacta — evitando mudanças em todos os pontos que consultam o objeto de configuração — é dividir a classe em pequenas partes usando mixins. No Python, um mixin não é uma construção sintática especial: trata-se de uma classe projetada para ser combinada por herança múltipla, adicionando comportamentos específicos.

Exemplo inicial

Imagine a seguinte classe legada. Ela carrega um arquivo TOML e expõe propriedades manualmente:

from pathlib import Path
from typing import Any, Dict
import tomllib

class AppConfiguration:
    def __init__(self) -> None:
        self._source = Path(__file__).resolve().parent.parent / "conf" / "settings.toml"
        self._data = self._load()

    def _load(self) -> Dict[str, Any]:
        if not self._source.exists():
            raise FileNotFoundError(f"Arquivo de configuração não encontrado: {self._source}")
        with self._source.open("rb") as stream:
            return tomllib.load(stream)

    @property
    def api_host(self) -> str:
        return self._data["api"]["host"]

    @property
    def api_port(self) -> int:
        return self._data["api"]["port"]

Com apenas algumas propriedades ainda é aceitável, mas, à medida que novos módulos são adicionados, a manutenção fica cada vez mais difícil.

Dividindo com mixins

A ideia é extrair grupos de propriedades relacionadas para classes separadas. Para evitar repetir lógica de acesso a valores aninhados, cria-se um mixin base com um helper genérico.

from typing import Any, Dict

class SettingsMixin:
    _data: Dict[str, Any]

    def _value(self, *path: str, default: Any = None) -> Any:
        node = self._data
        for key in path:
            if not isinstance(node, dict):
                return default
            node = node.get(key)
            if node is None:
                return default
        return node

Agora cada domínio da aplicação pode ter seu próprio mixin:

class APIMixin(SettingsMixin):
    @property
    def api_host(self) -> str:
        return self._value("api", "host", default="localhost")

    @property
    def api_port(self) -> int:
        return self._value("api", "port", default=8080)


class CacheMixin(SettingsMixin):
    @property
    def cache_host(self) -> str:
        return self._value("cache", "host", default="127.0.0.1")

    @property
    def cache_port(self) -> int:
        return self._value("cache", "port", default=6379)


class DatabaseMixin(SettingsMixin):
    @property
    def db_url(self) -> str:
        return self._value("database", "url", default="sqlite:///./app.db")

Por fim, a classe de configuração final herda apenas dos mixins necessários. O código que consome a configuração permanece inalterado.

import tomllib
from pathlib import Path

class AppConfiguration(APIMixin, CacheMixin, DatabaseMixin):
    def __init__(self) -> None:
        self._source = Path(__file__).resolve().parent.parent / "conf" / "settings.toml"
        self._data = self._load()

    def _load(self) -> Dict[str, Any]:
        if not self._source.exists():
            raise FileNotFoundError(f"Arquivo de configuração não encontrado: {self._source}")
        with self._source.open("rb") as stream:
            return tomllib.load(stream)

# uso
cfg = AppConfiguration()
print(cfg.api_host)   # APIMixin
print(cfg.cache_port) # CacheMixin
print(cfg.db_url)     # DatabaseMixin

Quando surgir a necessidade de adicionar novas configurações, basta criar um novo mixin e incluí-lo na lista de herança.

Agregando por domínio

Se a lista de mixins ficar longa, é possível criar uma camada intermediária que agrupa mixins relacionados. Isso mantém a classe principal enxuta.

class InfrastructureMixin(APIMixin, CacheMixin, DatabaseMixin):
    pass

class AppConfiguration(InfrastructureMixin):
    ...

Uma orgnaização de diretórios possível seria:

conf/
  settings.toml
src/
  config/
    __init__.py
    settings.py
    mixins/
      __init__.py
      api.py
      cache.py
      db.py

O arquivo __init__.py de src/config pode exportar a instância única de AppConfiguration, enquanto cada mixin vive em seu próprio módulo.

Variáveis de ambiente como alternativa

Para projetos novos, vale considerar armazenar a configuração em variáveis de ambiente, especialmente em aplicações executadas em contêineres ou Kubernetes. Essa abordagem elimina a leitura de arquivos durante a inicialização e facilita a separação entre código e configuração sensível. O custo é a dispersão das definições, o que pode dificultar a descoberta de valores durante o desenvolvimento local.

Tags: Python Mixins toml Refatoracao

Publicado em 6-24 04:58