博客 / 詳情

返回

使用Mixin類簡單重構配置模塊

前言

按照個人習慣,項目伊始我會按照如下結構組織項目配置,也就是配置文件放在conf/目錄,單獨寫一個配置模塊pkg/config.py去讀取加載。有的小項目還好,沒什麼配置項。但有的項目要調用很多第三方的接口,配置文件寫了一堆接口地址、認證方式等,配置模塊也相應增加了幾百行。看着這快上千行的配置模塊,還是儘早改改比較好。

conf/
  app.toml
pkg/
  config.py

有的項目會把配置打散,各個模塊維護各自的配置,但對於使用單一配置模塊的項目,除了配置模塊,其它模塊調用配置類單例的地方我都不想去碰,也懶得碰。這時候,使用Mixin類就比較合適。

在Python中,Mixin只是一種約定,語言層面沒有顯式支持,實際上就是python的多重繼承。

舊代碼的配置類

舊的配置模塊pkg/config.py大概長這樣,每個配置項都寫成了動態屬性。即便只是簡單的取值,也可能會寫很多。如果再加上校驗,單個文件的內容就會很多了,鼠標滾輪翻快點估計就找不到哪對哪了。

class Config:
    def __init__(self) -> None:
        self._config_file = Path(__file__).parent.parent.parent / "conf" / "config.toml"
        self._config = self._load_config()

    def _load_config(self) -> Dict[str, Any]:
        if not self._config_file.exists():
            raise FileNotFoundError(f"Configuration file {self._config_file} does not exist.")
        with open(self._config_file, "rb") as f:
            return tomllib.load(f)

    @property
    def service_host(self) -> str:
        return self._config.get("service").get("host", "127.0.0.1")

    @property
    def service_port(self) -> int:
        return self._config.get("service").get("port", 8000)

拆分

簡單示例

如果配置的層級特別深,Mixin裏寫一長串.get().get()也挺礙眼的。可以寫一個基類BaseMixin,在基類中定義一個遞歸讀取配置的方法。

class BaseMixin:
    _config: Dict[str, Any]
    
    def _get_conf(self, *keys: str, default: Any = None) -> Any:
        """遞歸獲取配置"""
        data = self._config
        for k in keys:
            if isinstance(data, dict):
                data = data.get(k)
            else:
                return default
        return data if data is not None else default

class FeatureMixin(BaseMixin):
    @property
    def is_feature_enabled(self) -> bool:
        return self._get_conf("module", "submodule", "enabled", default=False)
        
from typing import Any, Dict

class ServiceMixin(BaseMixin):
    """處理 Service 相關的配置項"""
    @property
    def service_host(self) -> str:
        return self._get_conf("service", "host", default="127.0.0.1")

    @property
    def service_port(self) -> int:
        return self._get_conf("service", "port", default=8000)

class DatabaseMixin(BaseMixin):
    """處理 Database 相關的配置項"""
    @property
    def db_url(self) -> str:
        return self._get_conf("database", "url", default="sqlite:///./test.db")

組合成最終的Config

import tomllib
from pathlib import Path

class Config(ServiceMixin, DatabaseMixin):
    """
    最終的聚合類。繼承了所有 Mixin,因此它擁有了所有定義好的 @property。
    """

    def __init__(self) -> None:
        self._config_file = Path(__file__).parent.parent.parent / "conf" / "config.toml"
        self._config = self._load_config()

    def _load_config(self) -> Dict[str, Any]:
        if not self._config_file.exists():
            raise FileNotFoundError(f"Configuration file {self._config_file} does not exist.")
        with open(self._config_file, "rb") as f:
            return tomllib.load(f)

# --- 調用端代碼完全不需要修改 ---
config = Config()
print(config.service_host)  # 來源於 ServiceMixin
print(config.db_url)        # 來源於 DatabaseMixin

如上改造後,調用方依然使用config.db_url 這樣的方式來使用,不用管配置模塊如何改動。以後如果再想新增配置,比如Redis的連接配置,只需要新增一個RedisMixin類,並加到Config的繼承列表裏即可。

中間層聚合

當配置的Mixin類越來越多,Config類會有一溜排的Mixin類要繼承,看着有點頭重腳輕。這時可以按邏輯領域先進行聚合。

比如,數據庫相關的先聚合成DBMixins(這種中間層聚合的Mixin類,推薦命名後綴為Mixins

# pkg/config/mixins/db.py

class PostgresMixin(BaseMixin):
    @property
    def pg_host(self) -> str:
        pass
        
    @property
    def pg_port(self) -> int:
        pass
        
class RedisMixin(BaseMixin):
    @property
    def redis_host(self) -> str:
        pass
        
    @property
    def redis_port(self) -> int:
        pass
        
class DBMixins(PostgresMixin, RedisMixin):
    pass

Config類中組裝

# pkg/config/config.py
from pkg.config.mixins.db import DBMixins
class Config(DBMixins):
    pass

最終目錄結構如下:

conf/
  config.toml
pkg/
  config/
    __init__.py  # 在此創建 Config 類的單例
    config.py    # 其中只有 Config 類
    mixins/
      __init__.py  # 定義 BaseMixin 基類
      db.py
      third_parties.py

補充

對於新項目,可以試試把配置全部放到環境變量,各個模塊實現各自的配置模塊。好處就是找起來方便,而且從環境變量中讀取配置也不用操心文件讀取的阻塞問題。目前很多運行在k8s上的服務喜歡用這種讀取環境變量的配置方式。缺點可能就是維護起來不太方便,畢竟配置被打散了,交接給別人的話,別人可能得到處找配置。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.