5. 结构型模式:装饰器与代理
这是什么:装饰器 = 把对象一层层包起来,每次包装叠加一点新职责;代理 = 在"你"和"真对象"之间站一个中间人,访问前后加控制。都是结构型:不改原类,包出来。
为什么用:给咖啡加奶、加糖,不可能为每种组合各写一个类(奶+糖、双份奶、奶+糖+冰……组合爆炸);查天气想加缓存,又不想把缓存代码塞进天气 API 类里弄脏它。包装模式让"新职责"和"原对象"彻底分离。
如何用:先用包装链给咖啡叠加配料,再写一个缓存代理给天气 API 提速。
装饰器模式:层层包装叠职责
Milk 和 Sugar 都"包住"一杯已有的饮料:价格在原价上加,描述在原文上接——同一杯咖啡想加几层加几层:
class Coffee:
def cost(self):
return 10
def desc(self):
return "咖啡"
class Milk:
"""加奶:包一层,价钱和描述都叠加"""
def __init__(self, drink):
self.drink = drink # 被包装的那杯
def cost(self):
return self.drink.cost() + 3
def desc(self):
return self.drink.desc() + "+奶"
class Sugar:
"""加糖:同样套路再包一层"""
def __init__(self, drink):
self.drink = drink
def cost(self):
return self.drink.cost() + 1
def desc(self):
return self.drink.desc() + "+糖"
order = Sugar(Milk(Coffee())) # 咖啡 → 加奶 → 加糖
print(f"{order.desc()}:{order.cost()} 元")
你会看到:
咖啡+奶+糖:14 元
cost() 的调用像剥洋葱:Sugar 问 Milk,Milk 问 Coffee,一层层往里要价再各自加价。组合(奶+糖、双份奶)随便叠,不用新建任何类。
你其实早用过这个模式:Python 的
@decorator语法就是装饰器思想的语法糖——@log、@app.route都是在"不改原函数的前提下包一层加功能"。
代理:访问前后加控制
代理和真对象接口长得一模一样,调用方察觉不出差别,但代理在中间偷偷做事——比如缓存:
class WeatherAPI:
"""真家伙:每次查询都要走网络(这里假装)"""
def query(self, city):
print(f"(网络请求:查询 {city}……)")
return f"{city} 晴 26°C"
class CachedProxy:
"""代理:一样的 query 接口,内部加了缓存"""
def __init__(self):
self.api = WeatherAPI()
self.cache = {} # 查过的结果存这里
def query(self, city):
if city not in self.cache:
self.cache[city] = self.api.query(city) # 第一次:真的去查
return self.cache[city] # 之后:直接回缓存
weather = CachedProxy()
print(weather.query("北京"))
print(weather.query("北京"))
print(weather.query("上海"))
你会看到:
(网络请求:查询 北京……)
北京 晴 26°C
北京 晴 26°C
(网络请求:查询 上海……)
上海 晴 26°C
第二次查北京没再出现"网络请求"——缓存代理把重复查询挡在了真 API 之外。除了缓存,代理还能做权限检查、延迟加载、请求计数,套路都一样。
标准库里就有现成的:
functools.lru_cache一个装饰器给任何函数装上缓存代理,先试它,再考虑手写。
装饰器 vs 代理:包装的目的不同
| 装饰器 | 代理 | |
|---|---|---|
| 包的目的 | 叠加职责(加价、加功能) | 控制访问(缓存、鉴权、延迟) |
| 层数 | 常多层嵌套 | 通常一层 |
| 调用方感知 | 知道在加料 | 感觉不到中间人存在 |
本章解读
| 代码 | 作用 |
|---|---|
self.drink = drink |
包装的核心:持有被包对象(组合) |
Sugar(Milk(Coffee())) |
从里往外包,从外往里调 |
CachedProxy 与 WeatherAPI 同名同参的 query |
接口一致,调用方无感切换 |
if city not in self.cache |
代理的典型逻辑:先查旁路,不命中才放行 |
记忆口诀
装饰叠职责,一层加一点;代理管通行,缓存放中间。包装不改动,组合胜继承。
动手改
- 写一个
Ice类(加冰 +2 元、+冰),链出Ice(Ice(Milk(Coffee())))双份冰,算算多少钱 - 把
Milk和Sugar合并成一个Addon(name, price, drink)通用配料类 - 给
CachedProxy加一句:缓存命中时打印"(缓存命中)",重跑验证
下一章:行为型模式:策略与观察者 🐸