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.