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

4612

积分

1

好友

630

主题
发表于 1 小时前 | 查看: 2| 回复: 0

utils.py 在项目初期可能很顺手。但几个月后再看,里面可能已经塞满了 40 多个函数:format_time()send_email()build_headers()load_yaml()retry_request()mask_phone()……谁都可以往里面添加代码,最后却谁都不敢轻易修改。

这种做法最令人困扰的,并非代码不够优雅,而是它会模糊功能的边界。例如,当你编写订单同步逻辑时,核心代码可能散落在多个 utils.xxx() 函数调用中。表面上看调用点很简洁,排查故障时却处处是陷阱。尤其是在 Python 这类约束较少的项目中,utils.py 极易演变成一个“公共垃圾场”。这种代码味道,经验丰富的开发者通常一眼就能识别。

若想真正让代码结构变得清晰,并非简单地将 utils.py 改名即可,关键在于将“变化的部分”清晰地分离出来。下面介绍的 3 个设计模式,足够应对常见场景,并且远比持续堆砌工具函数更为可靠。

1. 策略模式:告别无限膨胀的 if/elif

许多人在编写导出、计费或风控规则时,第一版代码往往是这样的:

def calc_fee(channel: str, amount: int) -> int:
    if channel == "wechat":
        return int(amount * 0.006)
    elif channel == "alipay":
        return int(amount * 0.0055)
    elif channel == "bank":
        return 2
    raise ValueError(f"unknown channel: {channel}")

起初只有三四个分支时尚可忍受。但当需要加入企业渠道、活动期特殊费率、海外卡通道等逻辑后,这段代码就会变得难以维护。再过一阵子,测试人员提出“只有使用 bank 渠道的老商户才沿用旧逻辑”这样的需求时,你就会意识到问题的严重性。

这种情况下,不应再将新逻辑塞进 utils.py,而是直接采用策略模式:

from abc import ABC, abstractmethod

class FeeStrategy(ABC):
    @abstractmethod
    def calc(self, amount: int) -> int:
        pass

class WechatFee(FeeStrategy):
    def calc(self, amount: int) -> int:
        return int(amount * 0.006)

class AlipayFee(FeeStrategy):
    def calc(self, amount: int) -> int:
        return int(amount * 0.0055)

class BankFee(FeeStrategy):
    def calc(self, amount: int) -> int:
        return 2

STRATEGIES = {
    "wechat": WechatFee(),
    "alipay": AlipayFee(),
    "bank": BankFee(),
}

def calc_fee(channel: str, amount: int) -> int:
    try:
        return STRATEGIES[channel].calc(amount)
    except KeyError:
        raise ValueError(f"unknown channel: {channel}")

这样做的好处不仅仅是“看起来更优雅”,更重要的是缩小了变更的影响范围。未来某个支付渠道的计费规则需要调整时,你只需修改对应的策略类,而无需担心在冗长的 if/elif 链中误改其他分支。对于线上核心逻辑,我宁愿多创建几个文件,也不愿继续维护一个万能的 utils.py

2. 工厂模式:集中管理对象创建逻辑

另一个典型的代码坏味道是,各种客户端初始化代码散落在业务逻辑各处:

def send_message(channel: str, to: str, content: str):
    if channel == "sms":
        client = SmsClient(api_key="xxx", timeout=3)
    elif channel == "email":
        client = EmailClient(host="smtp.xxx.com", port=465)
    else:
        raise ValueError("unsupported channel")

    client.send(to, content)

乍看之下似乎没问题,但实际上很容易失控。配置信息与业务逻辑混杂,对象的构造细节被多处复制。后续若需要增加重试机制、埋点或熔断逻辑,修改将变得异常繁琐。

工厂模式正是为了收拾这类“烂摊子”而存在的:

class NotifyFactory:
    @staticmethod
    def create(channel: str):
        if channel == "sms":
            return SmsClient(api_key="xxx", timeout=3)
        if channel == "email":
            return EmailClient(host="smtp.xxx.com", port=465)
        raise ValueError(f"unsupported channel: {channel}")

def send_message(channel: str, to: str, content: str):
    client = NotifyFactory.create(channel)
    client.send(to, content)

更进一步,工厂内部可以集成配置校验、实例复用、服务降级等逻辑。你会发现,真正的复杂性往往不在于 send() 这个调用本身,而在于“这个对象究竟该如何正确地创建出来”。既然对象的创建逻辑本身是易变的,那么就不应该将它散落在各处。

3. 装饰器模式:分离横切关注点

最容易被随意塞进 utils.py 的,还有一类“横切”关注点的逻辑,例如重试、耗时统计和异常告警。常见的写法可能是这样的:

def fetch_order(order_id: str):
    start = time.time()
    try:
        for _ in range(3):
            try:
                return request_order(order_id)
            except TimeoutError:
                continue
        raise TimeoutError("retry failed")
    finally:
        cost = int((time.time() - start) * 1000)
        print(f"fetch_order cost={cost}ms")

业务逻辑还未展开,各种辅助代码已经糊了一层。函数体越来越臃肿,后续查看日志也会变得困难。

对于这种场景,使用装饰器会非常合适:

import time
from functools import wraps

def retry(times: int = 3):
    def outer(func):
        @wraps(func)
        def inner(*args, **kwargs):
            last_exc = None
            for _ in range(times):
                try:
                    return func(*args, **kwargs)
                except TimeoutError as e:
                    last_exc = e
            raise last_exc
        return inner
    return outer

def record_cost(func):
    @wraps(func)
    def inner(*args, **kwargs):
        start = time.time()
        try:
            return func(*args, **kwargs)
        finally:
            cost = int((time.time() - start) * 1000)
            print(f"{func.__name__} cost={cost}ms")
    return inner

@record_cost
@retry(times=3)
def fetch_order(order_id: str):
    return request_order(order_id)

经过这样的改造,业务函数 fetch_order 就只保留了纯粹的业务逻辑。未来如果你想将简单的 print 输出替换为结构化的日志,或者将重试失败的信息上报到 Prometheus 监控系统,都不需要去翻阅每一个调用点进行修改。

归根结底,utils.py 最大的问题不在于文件名不够新颖,而在于它默认开发者“不想建立清晰的边界”。然而,只要项目生命周期稍微长一些,边界迟早都需要被明确界定,而越晚处理通常代价越高。

总结一下:策略模式用于收拢多变的业务分支,工厂模式用于统一管理对象的创建逻辑,装饰器模式则负责处理那些横跨多个功能的通用能力。这三种模式并非高深莫测的理论,它们常常是在实践中被无数 Bug “教育”出来的经验结晶。

当然,utils.py 并非绝对禁止使用,但它最好只存放那些真正稳定、无状态、且不会无限增长的纯工具函数。一旦其内容开始变得庞杂,就应该考虑进行拆分了。如果你在Python项目中遇到过类似的结构困境,不妨来云栈社区分享你的重构经验。




上一篇:被领导“架空”的同事,和那道让人头秃的“最小窗口子序列”算法题
下一篇:从 OpenAI 收购 Astral,看 AI 公司如何争夺开发者工具链
您需要登录后才可以回帖 登录 | 立即注册

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

GMT+8, 2026-3-21 06:27 , Processed in 0.543294 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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