使用Mixin类简单重构配置模块
<h2 id="前言">前言</h2><p>按照个人习惯,项目伊始我会按照如下结构组织项目配置,也就是配置文件放在<code>conf/</code>目录,单独写一个配置模块<code>pkg/config.py</code>去读取加载。有的小项目还好,没什么配置项。但有的项目要调用很多第三方的接口,配置文件写了一堆接口地址、认证方式等,配置模块也相应增加了几百行。看着这快上千行的配置模块,还是尽早改改比较好。</p>
<pre><code>conf/
app.toml
pkg/
config.py
</code></pre>
<p>有的项目会把配置打散,各个模块维护各自的配置,但对于使用单一配置模块的项目,除了配置模块,其它模块调用配置类单例的地方我都不想去碰,也懒得碰。这时候,使用Mixin类就比较合适。</p>
<p>在Python中,Mixin只是一种约定,语言层面没有显式支持,实际上就是python的多重继承。</p>
<h2 id="旧代码的配置类">旧代码的配置类</h2>
<p>旧的配置模块<code>pkg/config.py</code>大概长这样,每个配置项都写成了动态属性。即便只是简单的取值,也可能会写很多。如果再加上校验,单个文件的内容就会很多了,鼠标滚轮翻快点估计就找不到哪对哪了。</p>
<pre><code class="language-python">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:
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)
</code></pre>
<h2 id="拆分">拆分</h2>
<h3 id="简单示例">简单示例</h3>
<p>如果配置的层级特别深,Mixin里写一长串<code>.get().get()</code>也挺碍眼的。可以写一个基类<code>BaseMixin</code>,在基类中定义一个递归读取配置的方法。</p>
<pre><code class="language-python">class BaseMixin:
_config: Dict
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")
</code></pre>
<p>组合成最终的<code>Config</code>类</p>
<pre><code class="language-python">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:
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
</code></pre>
<p>如上改造后,调用方依然使用<code>config.db_url</code> 这样的方式来使用,不用管配置模块如何改动。以后如果再想新增配置,比如Redis的连接配置,只需要新增一个<code>RedisMixin</code>类,并加到<code>Config</code>的继承列表里即可。</p>
<h3 id="中间层聚合">中间层聚合</h3>
<p>当配置的Mixin类越来越多,<code>Config</code>类会有一溜排的Mixin类要继承,看着有点头重脚轻。这时可以按逻辑领域先进行聚合。</p>
<p>比如,数据库相关的先聚合成<code>DBMixins</code>(这种中间层聚合的Mixin类,推荐命名后缀为<code>Mixins</code>)</p>
<pre><code class="language-python"># 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
</code></pre>
<p>在<code>Config</code>类中组装</p>
<pre><code class="language-python"># pkg/config/config.py
from pkg.config.mixins.db import DBMixins
class Config(DBMixins):
pass
</code></pre>
<p>最终目录结构如下:</p>
<pre><code>conf/
config.toml
pkg/
config/
__init__.py# 在此创建 Config 类的单例
config.py # 其中只有 Config 类
mixins/
__init__.py# 定义 BaseMixin 基类
db.py
third_parties.py
</code></pre>
<h2 id="补充">补充</h2>
<p>对于新项目,可以试试把配置全部放到环境变量,各个模块实现各自的配置模块。好处就是找起来方便,而且从环境变量中读取配置也不用操心文件读取的阻塞问题。目前很多运行在k8s上的服务喜欢用这种读取环境变量的配置方式。缺点可能就是维护起来不太方便,毕竟配置被打散了,交接给别人的话,别人可能得到处找配置。</p>
</div>
<div id="MySignature" role="contentinfo">
<p>本文来自博客园,作者:花酒锄作田,转载请注明原文链接:https://www.cnblogs.com/XY-Heruo/p/19606287</p><br><br>
来源:https://www.cnblogs.com/XY-Heruo/p/19606287
頁:
[1]