找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4157

积分

0

好友

575

主题
发表于 昨天 04:07 | 查看: 11| 回复: 0

你们项目里是不是也有个叫 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_gethttp_postcall_* 之类的函数就可以移除了,只留下一些真正通用、与业务无关的纯工具函数,比如重试装饰器、通用签名算法等。所有与具体业务系统绑定的逻辑,都应该回到它们专属的客户端模块里。

外观模式的感觉就是:我给你一个简洁统一的入口,背后的复杂性被屏蔽起来,你只需要和这个入口交互

重构结果与准则

整个重构完成后,我做了个统计:原来的 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 吗?会的,但会非常克制:

  1. 坚决隔离业务逻辑:但凡与业务名词(如 OrderUser)相关的代码,坚决不放 utils
  2. 优先考虑模式化设计:对于需要扩展或变异的逻辑,优先思考能否用策略模式、工厂模式等来组织。
  3. 封装外部依赖:对于外部依赖多、易变的系统调用,封装成独立的客户端类,并通过网关统一管理。

Python项目中,utils.py 的滥用是一个常见陷阱。通过应用恰当的设计模式进行重构,可以显著提升代码的可读性、可维护性和可测试性。关键在于识别代码中的“坏味道”,并勇敢地对其动刀。下次当你打开自己项目里那个臃肿的 utils.py 时,不妨想想,是不是也可以用这些模式给它做个“瘦身手术”?如果你有更好的重构经验或想法,欢迎在云栈社区的技术论坛板块与其他开发者交流探讨。




上一篇:从“功能防滑”到“情感叙事”:设计师必须掌握的工业产品肌理重塑指南
下一篇:从“值不值20万”到LeetCode 665:非递减数列数组调整策略详解
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-10 09:25 , Processed in 0.418532 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表