找回密码
立即注册
搜索
发回帖 发新帖

6450

积分

0

好友

820

主题
发表于 12 小时前 | 查看: 6| 回复: 0

“上周帮团队 review 一段订单处理代码,密密麻麻的 if-else 判断支付渠道、导出格式、通知方式……新加一个微信支付要改 5 处地方,单元测试直接挂了 4 个。”

你是不是也遇到过类似的场景?代码能跑,但每次加功能都像拆弹。

其实,设计模式不是面试八股文——7 个 Python 原生支持的套路,就能把“意大利面式代码”变成清晰可扩展的工程。读完这篇,你也能写出同事直呼“优雅”的 Python 代码。

1. 单例(Singleton):全局唯一的“独生子”

类比:公司只有一台打印机,所有人共享使用。谁去打印都拿到同一台机器,不会出现“A 加纸了,B 却拿到空机器”的混乱。

正式定义:确保一个类只有一个实例,并提供全局访问点。

代码实战(Python 3.8+)

class Database:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(Database, cls).__new__(cls)
            cls._instance.connection = "Connected to DB"  # 模拟连接
        return cls._instance

# 验证
db1 = Database()
db2 = Database()
print(db1 is db2)  # True,同一个对象
print(db1.connection)  # Connected to DB

⚠️ 注意:90% 的人会踩坑
单例在多线程环境下可能失效,多个线程可能同时进入 if cls._instance is None。生产环境请使用 threading.Lock 或元类实现。另外,单元测试会因状态泄漏而互相影响——每个测试用例需要重置实例,否则前面的 mock 会污染后面的断言。

适用场景:数据库连接池、全局配置对象、日志记录器。
不适用:异步任务、多线程频繁读写的状态。

2. 工厂方法(Factory Method):生产对象的“流水线”

类比:你去快餐店点餐,告诉店员“要汉堡”,他不会问你“要哪种面包、肉饼怎么做”,而是直接给你对应的汉堡。你不需要知道生产细节。

正式定义:定义一个创建对象的接口,但由子类决定实例化哪一个类。

代码实战

class Exporter:
    """导出器基类"""
    def export(self, data):
        raise NotImplementedError

class PDFExporter(Exporter):
    def export(self, data):
        return f"将 {data} 导出为 PDF"

class CSVExporter(Exporter):
    def export(self, data):
        return f"将 {data} 导出为 CSV"

# 工厂函数
def exporter_factory(kind: str) -> Exporter:
    if kind == "pdf":
        return PDFExporter()
    elif kind == "csv":
        return CSVExporter()
    else:
        raise ValueError(f"不支持格式: {kind}")

# 使用
exporter = exporter_factory("pdf")
print(exporter.export("年度报表"))  # 将 年度报表 导出为 PDF

复杂度:时间 O(1),空间 O(1),不计对象内部数据。

扩展思考:国内报表系统常用 POI 导出 Excel,可以用工厂模式统一 ExcelExporter。新加一种格式,如 JSON,只需新增类,不必修改已有的 if-else,符合开闭原则。但 Python 中如果类型很少,直接用字典映射更简单:

exporters = {"pdf": PDFExporter, "csv": CSVExporter}
exporter = exporters.get("pdf", PDFExporter)()

3. 观察者(Observer):事件触发的“广播系统”

类比:你关注了 B 站 UP 主,每次他发视频,你和其他粉丝都会收到通知。你不需要轮询“他发新视频了吗”,而是 UP 主主动推给你。

正式定义:定义一对多的依赖关系,当一个对象状态改变时,所有依赖它的对象都会自动收到通知。

代码实战

class OrderEvent:
    """订单事件中心"""
    def __init__(self):
        self.subscribers = []  # 订阅者列表

    def subscribe(self, callback):
        self.subscribers.append(callback)

    def notify(self, order_data):
        for callback in self.subscribers:
            callback(order_data)

# 定义两个观察者行为
def send_email(order):
    print(f"发送邮件通知:订单 {order} 已创建")

def update_analytics(order):
    print(f"更新数据看板:订单 {order} 计入统计")

# 使用
event = OrderEvent()
event.subscribe(send_email)
event.subscribe(update_analytics)

event.notify("Order#123")
# 输出:
# 发送邮件通知:订单 Order#123 已创建
# 更新数据看板:订单 Order#123 计入统计

⚠️ 调试噩梦:当你有十几个订阅者时,一个 notify 调用会触发一连串行为,堆栈很难追踪。建议给每个回调加唯一 ID,并在日志中记录调用链。

适用场景:消息队列、Webhook、GUI 事件响应。国内常用 RocketMQ 或 Redis Pub/Sub 实现分布式观察者。

4. 策略(Strategy):可插拔的“算法切换器”

类比:打车回家,可以选“快车”“专车”“拼车”。计价算法不同,但你的操作,也就是输入起点终点,完全一样。策略就是“选哪个算法”。

正式定义:定义一系列算法,将每个算法封装起来,并使它们可以互相替换。

代码实战(对比错误与正确写法)

❌ 错误做法:硬编码 if-else

def get_price(base_price, strategy_type):
    if strategy_type == "discount":
        return base_price * 0.9
    elif strategy_type == "surge":
        return base_price * 1.5
    # 每次加策略都要改这个函数

✅ 最佳实践:策略类 + 运行时替换

from abc import ABC, abstractmethod

class PricingStrategy(ABC):
    @abstractmethod
    def calculate(self, price):
        pass

class DiscountStrategy(PricingStrategy):
    def calculate(self, price):
        return price * 0.9

class SurgeStrategy(PricingStrategy):
    def calculate(self, price):
        return price * 1.5

class PriceCalculator:
    def __init__(self, strategy: PricingStrategy):
        self.strategy = strategy

    def get_price(self, base_price):
        return self.strategy.calculate(base_price)

# 使用
calc = PriceCalculator(DiscountStrategy())
print(calc.get_price(100))  # 90.0

# 运行时切换策略
calc.strategy = SurgeStrategy()
print(calc.get_price(100))  # 150.0

复杂度:每个策略 O(1),替换策略 O(1)。

国内实践:双十一的“满减策略”“限时折扣策略”“会员价策略”都适合用策略模式。但注意 Python 中可以直接传函数对象,也就是一等公民,不一定非要写类:

def discount(price):
    return price * 0.9

def surge(price):
    return price * 1.5

class PriceCalculator:
    def __init__(self, strategy_func):
        self.strategy = strategy_func

5. 装饰器(Decorator):无侵入的“能力增强器”

类比:手机贴膜——不改变手机内部电路,但增加了防摔功能。装饰器就是在不修改原函数代码的前提下,附加新能力。

正式定义:动态地给一个对象添加一些额外的职责。

代码实战(FastAPI 鉴权场景)

def auth_required(fn):
    """鉴权装饰器:只有 admin 可以调用"""
    def wrapper(user, *args, **kwargs):
        if user != "admin":
            raise PermissionError(f"用户 {user} 无权限")
        return fn(user, *args, **kwargs)
    return wrapper

@auth_required
def delete_user(user, user_id):
    print(f"{user} 删除了用户 {user_id}")

# 正确调用
delete_user("admin", 42)  # admin 删除了用户 42

# 错误调用(会抛出异常)
# delete_user("guest", 42)  # PermissionError

复杂度:装饰器本身 O(1),但多层嵌套,如 @auth @log @cache,会增加调用栈深度,调试时需用 functools.wraps 保留原函数元数据。

⚠️ 注意:装饰器执行顺序是从下往上,靠近函数的先执行。另外,装饰器内的 wrapper 如果不返回原函数返回值,会吞掉结果。正确的装饰器应该写成:

from functools import wraps

def auth_required(fn):
    @wraps(fn)
    def wrapper(user, *args, **kwargs):
        ...
        return fn(user, *args, **kwargs)
    return wrapper

Python 原生支持:@staticmethod、@classmethod、@property 都是装饰器。国内常用于 FastAPI 依赖注入、Django 权限校验、日志埋点。想系统理解装饰器、迭代器与协程等高阶语法,可以结合 设计模式 一起看。

6. 命令(Command):延迟执行的“任务卡”

类比:餐厅里服务员写下订单小票,也就是命令对象,后厨按顺序执行,不需要知道这道菜是谁点的。支持撤销、重试、排队。

正式定义:将请求封装为对象,从而支持参数化、队列、日志和撤销操作。

代码实战(后台任务队列)

from abc import ABC, abstractmethod

class Command(ABC):
    @abstractmethod
    def execute(self):
        pass

class SendInvoiceCommand(Command):
    def execute(self):
        print("发送发票邮件")

class ResizeImageCommand(Command):
    def execute(self):
        print("压缩图片尺寸")

# 构建任务队列
queue = []
queue.append(SendInvoiceCommand())
queue.append(ResizeImageCommand())

# 统一执行
for job in queue:
    job.execute()

扩展:带重试的命令

class RetryCommand(Command):
    def __init__(self, cmd, max_retries=3):
        self.cmd = cmd
        self.retries = max_retries

    def execute(self):
        for i in range(self.retries):
            try:
                self.cmd.execute()
                break
            except Exception as e:
                print(f"重试 {i+1} 失败: {e}")

适用场景:Celery 任务、撤销/重做,如 Ctrl+Z、宏命令录制。国内常用 APScheduler + 命令(Command) 模式实现延迟任务。

7. 适配器(Adapter):接口不兼容的“转换插头”

类比:你带了一个三脚插头,也就是 PayPal 接口,但墙上只有两孔插座,也就是 Razorpay 接口。适配器,也就是转换插头,让两者能合作。

正式定义:将一个类的接口转换成客户希望的另一个接口,使原本不兼容的类能一起工作。

代码实战(统一支付网关)

# 两个第三方 SDK,接口不同
class PayPal:
    def send_payment(self, amount):
        print(f"PayPal 支付 {amount} 元")

class Alipay:
    def transfer(self, amount):
        print(f"支付宝支付 {amount} 元")

# 适配器:统一为 pay(amount)
class PaymentAdapter:
    def __init__(self, provider):
        self.provider = provider

    def pay(self, amount):
        if hasattr(self.provider, 'send_payment'):
            self.provider.send_payment(amount)
        elif hasattr(self.provider, 'transfer'):
            self.provider.transfer(amount)
        else:
            raise TypeError("不支持的支付提供方")

# 业务代码只依赖 pay 方法
def checkout(adapter: PaymentAdapter, amount):
    adapter.pay(amount)

# 使用
paypal_adapter = PaymentAdapter(PayPal())
alipay_adapter = PaymentAdapter(Alipay())

checkout(paypal_adapter, 100)  # PayPal 支付 100 元
checkout(alipay_adapter, 200)  # 支付宝支付 200 元

⚠️ 注意:适配器不是万能胶。如果两个接口语义相差太大,比如一个需要异步回调,另一个是同步返回,强行适配会引入诡异 bug。此时应考虑封装整个第三方调用为统一服务层,而不是逐个方法适配。

国内常见场景:对接微信支付、支付宝、银联云闪付;或统一不同云厂商的 OSS 接口,如阿里云 OSS vs 腾讯云 COS。

写在最后

设计模式不是为了用而用。我见过有人为了“优雅”在 10 行代码里塞进 3 个模式,结果新人接手直接自闭。过度设计的危害不亚于面条代码。一个简单的经验:当你第三次写相似的 if-else 或复制粘贴相同逻辑时,再考虑引入模式。

另外,Python 的鸭子类型和一等函数已经简化了很多经典模式,比如策略可以直接传 lambda,观察者可以用 Event 库。不要强行套用 Java 风格的类图,写出“Pythonic”的模式才是真本事。

记忆点回顾

  1. 单例:全局唯一实例,注意线程安全和测试隔离。
  2. 工厂方法:将对象创建与使用分离,扩展新类型无需修改旧代码。
  3. 观察者:事件驱动解耦,但调试困难,适合一对多通知。
  4. 策略:运行时切换算法,Python 中可直接传函数简化。
  5. 装饰器:Python 原生支持,无侵入增强函数,注意顺序和 wraps。
  6. 命令:封装操作为对象,支持队列、重试、撤销。
  7. 适配器:统一异构接口,接入第三方库时必备。

你在项目中用过哪个设计模式解决了实际问题?或者踩过什么“过度设计”的坑?欢迎在评论区分享。想继续深入 Python 工程实践,也可以到 云栈社区 交流。




上一篇:DuckDB 官方 skills 实测:Claude Code 自然语言查 SQL,记得核对日期口径
下一篇:向量数据库已死?Claude Code、Cursor 为什么集体抛弃 RAG
您需要登录后才可以回帖 登录 | 立即注册

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

GMT+8, 2026-10-9 19:09 , Processed in 0.083710 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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