你们项目里是不是也有个叫 utils.py 的文件?点开一看一千多行,逻辑散乱,谁改谁头大。前几天我帮同事排查一个线上问题,就因为一个小改动藏在一个名为 do_something_very_important 的工具函数里,费了半天劲才找到。这让我想起以前被 Spring Boot 默认配置坑的经历——问题本身不难,恶心的是排查过程。
一上头,我就把他那个庞大的 utils.py 给拆了,顺手应用了三种设计模式。拆完后的代码,用一个词形容就是:清爽。下面跟大家分享一下我是怎么做的。
场景与痛点
项目里有一堆数据导出需求,比如导出Excel、CSV、JSON,原先全挤在一个 utils.py 里:
# utils.py 里原来的样子
def export_excel(data, file_path):
# 各种格式化、各种 if
...
def export_csv(data, file_path):
...
def export_json(data, file_path):
...
def export_data(format_type, data, file_path):
if format_type == "excel":
return export_excel(data, file_path)
elif format_type == "csv":
return export_csv(data, file_path)
elif format_type == "json":
return export_json(data, file_path)
else:
raise ValueError("不支持的格式")
问题很明显:函数满天飞,随处可调用。一旦业务逻辑需要调整,就得全局搜索 “export_”,改完还怕影响到其他地方。这种“万能工具人”模块,活干得多,锅也背得勤。
我动第一刀,用的是策略模式。
模式一:策略模式 (Strategy Pattern)
别被教科书上的复杂定义吓到,策略模式的核心思想很简单:将“如何导出”这部分可变逻辑,封装成一个个可以互换的对象,从而取代那个冗长的 if-else 链条。
我是这样改造的。首先,定义一个抽象的导出策略接口和几个具体实现:
# export_strategies.py
from abc import ABC, abstractmethod
class ExportStrategy(ABC):
@abstractmethod
def export(self, data, file_path: str) -> None:
...
class ExcelExportStrategy(ExportStrategy):
def export(self, data, file_path: str) -> None:
# 这里写真正的 Excel 导出逻辑
print(f"[excel] 写入 {file_path}")
class CsvExportStrategy(ExportStrategy):
def export(self, data, file_path: str) -> None:
print(f"[csv] 写入 {file_path}")
class JsonExportStrategy(ExportStrategy):
def export(self, data, file_path: str) -> None:
print(f"[json] 写入 {file_path}")
然后,创建一个简单的“策略选择器”,彻底告别 if-elif-else 火车:
# exporter.py
from export_strategies import (
ExcelExportStrategy,
CsvExportStrategy,
JsonExportStrategy,
ExportStrategy,
)
_STRATEGY_MAP: dict[str, ExportStrategy] = {
"excel": ExcelExportStrategy(),
"csv": CsvExportStrategy(),
"json": JsonExportStrategy(),
}
def export_data(format_type: str, data, file_path: str) -> None:
try:
strategy = _STRATEGY_MAP[format_type]
except KeyError:
raise ValueError(f"不支持的格式:{format_type}")
strategy.export(data, file_path)
这么一改,好处立刻显现:
- 扩展性极佳:需要新增 XML 导出?只需添加一个
XmlExportStrategy 类,并在映射表里注册 _STRATEGY_MAP[“xml”] = XmlExportStrategy() 即可。
- 逻辑集中:项目里散落的 if-else 判断消失了,所有导出调用都统一通过
export_data 函数入口。
- 易于测试:编写单元测试时,可以直接实例化一个
FakeExportStrategy 进行模拟,无需操作真实文件系统。
这就是第一个模式——策略模式。你可以把它理解为一种“可插拔的 if-else”机制。
模式二:工厂模式 (Factory Pattern) 提炼配置对象
第二个要拆的是 utils.py 里一坨负责“解析配置”的工具函数。原来长这样:
# utils.py 里的另一个大家伙
def parse_config(config: dict):
# 这个函数长得像未完结的小说
...
任何需要从配置字典里计算点什么的代码都调用它。问题在于,你根本不清楚这个庞然大物内部依赖了多少全局状态,改动它就像在雷区跳舞。
这次我采用 工厂模式 的思路,配合创建小对象。做法很直接:
# config_readers.py
class DbConfig:
def __init__(self, url: str, pool_size: int) -> None:
self.url = url
self.pool_size = pool_size
class CacheConfig:
def __init__(self, host: str, ttl: int) -> None:
self.host = host
self.ttl = ttl
class ConfigFactory:
@staticmethod
def create_db_config(raw: dict) -> DbConfig:
# 这里可以做校验、设置默认值等
return DbConfig(
url=raw["DB_URL"],
pool_size=int(raw.get("DB_POOL_SIZE", 10)),
)
@staticmethod
def create_cache_config(raw: dict) -> CacheConfig:
return CacheConfig(
host=raw["CACHE_HOST"],
ttl=int(raw.get("CACHE_TTL", 60)),
)
业务代码因此变得干净明了,不再需要去触碰那个黑盒般的 utils.parse_config:
# somewhere in service
from config_readers import ConfigFactory
def init_app(settings: dict):
db_config = ConfigFactory.create_db_config(settings)
cache_config = ConfigFactory.create_cache_config(settings)
print("db:", db_config.url, db_config.pool_size)
print("cache:", cache_config.host, cache_config.ttl)
为什么这里要用工厂?核心目的是:封装对象创建的复杂细节,让调用者无需关心内部逻辑。
过去大家可能到处复制粘贴这样的代码:
db_url = settings["DB_URL"]
pool = int(settings.get("DB_POOL_SIZE", 10))
# 同样的代码在十处地方重复出现
现在只需记住一条规则:“需要数据库配置?去找工厂要。” 至此,utils.py 里与配置解析相关的大部分混乱代码都被迁移走了。
模式三:外观模式 (Facade Pattern) 统一外部接口
第三个让我无法忍受的,是他们项目里那个“接口调用工具集”。你们肯定见过类似的:
# utils.py
def http_get(url, headers=None, timeout=3):
...
def http_post(url, data=None, headers=None, timeout=3):
...
def call_xxx_system(payload):
# 这里面各种拼接URL,加签名,埋点
...
这注定会沦为垃圾场:各个外部系统的调用逻辑都往里塞 call_*_system,最终演变成一个跨团队的“公共坟场”,难以维护。
我的解决方案带有 外观模式 和一点 适配器模式 的色彩。别纠结名词,思路就一句:为每个外部系统创建专门的客户端类,并提供一个统一的入口网关。
以“支付系统”为例,为其创建独立的客户端:
# clients/payment_client.py
import requests
class PaymentClient:
def __init__(self, base_url: str, token: str) -> None:
self._base_url = base_url.rstrip("/")
self._token = token
def _headers(self) -> dict:
return {
"Authorization": f"Bearer {self._token}",
}
def create_order(self, amount: int, user_id: str) -> dict:
resp = requests.post(
f"{self._base_url}/orders",
json={"amount": amount, "user_id": user_id},
headers=self._headers(),
timeout=3,
)
resp.raise_for_status()
return resp.json()
def query_order(self, order_id: str) -> dict:
resp = requests.get(
f"{self._base_url}/orders/{order_id}",
headers=self._headers(),
timeout=3,
)
resp.raise_for_status()
return resp.json()
接着,创建一个“门面”(Gateway),聚合所有外部系统的客户端:
# gateway.py
from clients.payment_client import PaymentClient
# from clients.sms_client import SmsClient # 假设还有其他客户端
class ExternalGateway:
def __init__(self, settings: dict) -> None:
self.payment = PaymentClient(
base_url=settings["PAYMENT_URL"],
token=settings["PAYMENT_TOKEN"],
)
# self.sms = SmsClient(...)
# 使用示例
gateway = ExternalGateway(settings)
def pay(user_id: str, amount: int):
order = gateway.payment.create_order(amount=amount, user_id=user_id)
return order["id"]
现在再看,原来的 utils.py 里那些 http_get、http_post、call_* 之类的函数就可以移除了,只留下一些真正通用、与业务无关的纯工具函数,比如重试装饰器、通用签名算法等。所有与具体业务系统绑定的逻辑,都应该回到它们专属的客户端模块里。
外观模式的感觉就是:我给你一个简洁统一的入口,背后的复杂性被屏蔽起来,你只需要和这个入口交互。
重构结果与准则
整个重构完成后,我做了个统计:原来的 utils.py 有 1800 多行,改造后瘦身到不足 200 行,剩下的都是一些“小而美”的纯函数,例如这个重试装饰器:
def retry(times=3, exceptions=(Exception,)):
def decorator(fn):
def wrapper(*args, **kwargs):
last = None
for _ in range(times):
try:
return fn(*args, **kwargs)
except exceptions as e:
last = e
raise last
return wrapper
return decorator
这种函数留在 utils 里是合理的:它们与具体业务无关,是纯粹的技术工具。而真正承载业务的逻辑——如导出策略、配置工厂、外部系统客户端——都已回归到各自语义清晰的模块中。
那么,现在还会写 utils.py 吗?会的,但会非常克制:
- 坚决隔离业务逻辑:但凡与业务名词(如
Order, User)相关的代码,坚决不放 utils。
- 优先考虑模式化设计:对于需要扩展或变异的逻辑,优先思考能否用策略模式、工厂模式等来组织。
- 封装外部依赖:对于外部依赖多、易变的系统调用,封装成独立的客户端类,并通过网关统一管理。
在Python项目中,utils.py 的滥用是一个常见陷阱。通过应用恰当的设计模式进行重构,可以显著提升代码的可读性、可维护性和可测试性。关键在于识别代码中的“坏味道”,并勇敢地对其动刀。下次当你打开自己项目里那个臃肿的 utils.py 时,不妨想想,是不是也可以用这些模式给它做个“瘦身手术”?如果你有更好的重构经验或想法,欢迎在云栈社区的技术论坛板块与其他开发者交流探讨。