Python 设计模式——外观模式(Facade Pattern)详解
本文面向零基础新手,从概念到“简化入口”、从简单示例到完整子系统,全方位讲解外观模式。
一、什么是外观模式?
1.1 通俗理解
外观模式是一种结构型设计模式,核心思想是:
为一个复杂的子系统(多个类、多个接口)提供一个统一的、简化的“外观”接口;客户只和这个外观打交道,由外观在内部去调用子系统的各个部分,从而让客户不必了解子系统内部有多少类、调用顺序如何,降低使用难度和耦合。
可以这样类比:
- 现实中的外观:看电影要开投影、关灯、开音响、拉窗帘、选片……若你每次自己操作七八个设备,很麻烦。影院提供一个“一键观影”按钮(外观):你只按一下,内部按正确顺序把投影、灯、音响、窗帘都处理好。你不需要知道先关灯还是先开投影,一个入口、一套简单操作就行。
- 代码里:下单要查库存、扣库存、调支付、创建订单、发通知、扣积分……若业务代码到处直接调库存类、支付类、订单类,会乱且难维护。做一个“订单外观”:对外只暴露
place_order(用户, 商品, 数量),内部按顺序调用库存、支付、订单、通知等,客户只调一个方法。
所以,外观模式解决的是:子系统复杂、调用方不想(或不该)关心内部细节和调用顺序时,用一个“门面”把复杂操作包起来,提供简单、统一的入口。
1.2 为什么需要外观?
场景一:子系统类多、调用复杂
例如“启动应用”:要读配置、初始化数据库连接、初始化日志、注册路由、启动服务器……若每处启动代码都自己写这一串,容易漏步骤、顺序错。做一个 ApplicationFacade.start(),内部按固定流程调用各个模块,一处实现、到处复用。
场景二:降低客户与子系统的耦合
客户本来要依赖 N 个类(库存、支付、物流、通知),现在只依赖一个外观类;以后子系统内部重构(如合并两个类、改调用顺序),只要外观的对外接口不变,客户代码不用改。
场景三:分层清晰
外观成为“子系统对外的唯一入口”,便于理解:要做事就找外观,外观再协调内部。也便于测试:可以 mock 外观,或只测外观的编排逻辑。
1.3 适用场景(什么时候用?)
| 场景 | 说明 |
|---|---|
| 子系统复杂、调用步骤多 | 希望“一步到位”完成一系列操作,而不在业务代码里写一长串调用。 |
| 要降低客户与子系统的耦合 | 客户只依赖外观,不直接依赖内部多个类。 |
| 要为子系统提供清晰入口 | 分层、模块化,外观即“门面”。 |
| 遗留系统包装 | 老系统接口混乱,用外观包一层,对外提供简洁接口。 |
简单记忆:有一堆类、一堆步骤,希望一个简单入口完成一串操作时,用外观。
二、外观模式的结构(两个角色)
| 角色 | 说明 |
|---|---|
| 外观(Facade) | 对外提供少量、简单的方法;内部持有或创建子系统的各个组件,在方法里按顺序调用它们,完成一个完整流程。 |
| 子系统(Subsystem) | 多个类/模块,各自负责一部分功能;外观负责“编排”它们,客户不直接调它们(或只在少数场景下直接调)。 |
关系可以理解为:
- 客户只依赖 外观,调用外观的方法(如
start()、place_order())。 - 外观内部依赖 子系统的多个类,在实现里依次调用:先 A,再 B,再 C……客户不需要知道 A、B、C 的存在和顺序。
三、示例一:一键观影(最经典)
需求:观影要:开投影、关灯、开音响、拉窗帘、播放。客户不想记顺序,只要“开始观影”;结束后要“结束观影”:关投影、开灯、关音响、拉窗帘。
3.1 子系统类(多个、各自独立)
class Projector:
def on(self):
print("投影仪 开")
def off(self):
print("投影仪 关")
class Light:
def on(self):
print("灯 开")
def off(self):
print("灯 关")
class Sound:
def on(self):
print("音响 开")
def off(self):
print("音响 关")
class Curtain:
def down(self):
print("窗帘 拉下")
def up(self):
print("窗帘 拉开")
class Player:
def play(self, name: str):
print(f"播放: {name}")
def stop(self):
print("停止播放")
3.2 外观:统一入口
class HomeTheaterFacade:
"""家庭影院外观:一键观影、一键结束"""
def __init__(self):
self._projector = Projector()
self._light = Light()
self._sound = Sound()
self._curtain = Curtain()
self._player = Player()
def watch_movie(self, name: str):
"""开始观影:按正确顺序启动各设备"""
print("=== 开始观影 ===")
self._curtain.down()
self._light.off()
self._projector.on()
self._sound.on()
self._player.play(name)
def end_movie(self):
"""结束观影:按正确顺序关闭"""
print("=== 结束观影 ===")
self._player.stop()
self._sound.off()
self._projector.off()
self._light.on()
self._curtain.up()
3.3 客户只调外观
facade = HomeTheaterFacade()
facade.watch_movie("泰坦尼克号")
# === 开始观影 ===
# 窗帘 拉下 / 灯 关 / 投影仪 开 / 音响 开 / 播放: 泰坦尼克号
facade.end_movie()
# === 结束观影 ===
# 停止播放 / 音响 关 / 投影仪 关 / 灯 开 / 窗帘 拉开
要点:客户不接触 Projector、Light、Sound 等,只调 watch_movie、end_movie;顺序和细节都在外观里,改顺序只改外观即可。
四、示例二:下单流程(库存 + 支付 + 订单 + 通知)
需求:下单要:检查库存、扣库存、扣款、创建订单、发通知。用外观把这一串包成 place_order(用户, 商品ID, 数量)。
4.1 子系统(简化)
class Inventory:
def check(self, product_id: str, qty: int) -> bool:
print(f" [库存] 检查 {product_id} x{qty}")
return True
def deduct(self, product_id: str, qty: int):
print(f" [库存] 扣减 {product_id} x{qty}")
class Payment:
def pay(self, user_id: str, amount: float) -> bool:
print(f" [支付] 用户 {user_id} 支付 {amount} 元")
return True
class OrderService:
def create(self, user_id: str, product_id: str, qty: int, amount: float):
print(f" [订单] 创建订单 user={user_id} product={product_id} qty={qty} amount={amount}")
class Notifier:
def send(self, user_id: str, msg: str):
print(f" [通知] -> {user_id}: {msg}")
4.2 外观:下单
class OrderFacade:
def __init__(self):
self._inventory = Inventory()
self._payment = Payment()
self._order = OrderService()
self._notifier = Notifier()
def place_order(self, user_id: str, product_id: str, qty: int, unit_price: float) -> bool:
"""下单:检查库存 -> 扣库存 -> 支付 -> 创建订单 -> 通知"""
if not self._inventory.check(product_id, qty):
return False
self._inventory.deduct(product_id, qty)
amount = qty * unit_price
if not self._payment.pay(user_id, amount):
return False
self._order.create(user_id, product_id, qty, amount)
self._notifier.send(user_id, f"订单已创建,金额 {amount} 元")
return True
4.3 使用
facade = OrderFacade()
facade.place_order("user1", "P001", 2, 99.0)
# 客户只调一行,内部按顺序走完库存、支付、订单、通知
五、示例三:应用启动外观
需求:启动应用要:加载配置、初始化数据库、初始化日志、注册路由、启动 HTTP 服务。用外观包成 start()。
class ConfigLoader:
def load(self):
print(" [配置] 加载完成")
class Database:
def connect(self):
print(" [数据库] 连接完成")
class Logger:
def init(self):
print(" [日志] 初始化完成")
class Router:
def register(self):
print(" [路由] 注册完成")
class HttpServer:
def run(self):
print(" [HTTP] 服务已启动")
class ApplicationFacade:
def __init__(self):
self._config = ConfigLoader()
self._db = Database()
self._logger = Logger()
self._router = Router()
self._server = HttpServer()
def start(self):
print("=== 启动应用 ===")
self._config.load()
self._logger.init()
self._db.connect()
self._router.register()
self._server.run()
print("=== 启动完成 ===")
# 使用
app = ApplicationFacade()
app.start()
客户只调 app.start(),不必关心先配配置还是先连数据库;顺序变更只改外观。
六、外观 vs 其他模式
| 模式 | 目的 | 与外观的区别 |
|---|---|---|
| 外观 | 为子系统提供简化、统一的入口 | 不改变子系统接口,只是“包一层”、编排调用。 |
| 适配器 | 让不兼容的接口能一起用 | 侧重接口转译;外观侧重“简化调用”、不转译接口。 |
| 中介者 | 多对象之间互相通信由中介协调 | 中介者管的是“对象间交互”;外观是“客户 -> 外观 -> 子系统”的单向编排。 |
简单记忆:外观 = 一个简单入口 + 内部按顺序调多个类,不改变子系统类的接口。
七、常见问题与注意点
7.1 客户还能直接调子系统吗?
可以。外观只是“推荐入口”,并不禁止客户在特殊场景下直接使用子系统类;但常规流程应通过外观,以便统一维护顺序和逻辑。
7.2 外观会不会变成“上帝类”?
若子系统非常庞大,一个外观里塞满几十个类、几十个步骤,外观会变得难维护。可以按“子流程”拆成多个小外观(如 OrderFacade、RefundFacade),或让外观只编排“子模块的入口”,子模块内部再有自己的小外观或流程。
7.3 何时用外观、何时直接调?
当“一系列步骤”在多处重复、或希望客户与子系统解耦、或需要统一入口时,用外观;当只是单次、简单调用一两个类时,直接调即可。
八、小结
- 外观模式:为复杂子系统提供一个简化的统一入口(外观类);客户只和外观打交道,外观在内部按顺序调用子系统的多个类,完成完整流程,从而降低使用难度、降低客户与子系统的耦合。
- 两个角色:外观(Facade)、子系统(多个类);客户只依赖外观。
- 典型用途:一键观影、下单流程、应用启动、编译流程等“多步骤、多类协作”的场景。
- 实现要点:外观持有或创建子系统组件,在对外方法里按固定顺序调用它们。
建议先写“一键观影”或“应用启动”一例,再写“下单外观”,体会“一个方法里编排多步、客户只调一个入口”,这样对外观模式会掌握得比较扎实。