一、外观模式:统一接口、子系统封装
外观模式,说白了就是给一堆复杂的子系统提供一个简单的入口。你想想看,我们平时用电脑,开机只需要按一下电源键,背后主板自检、内存加载、硬盘启动、显卡初始化……这些乱七八糟的流程,你根本不用管。电源键就是外观。
我个人习惯把外观模式叫做"门面模式"——Facade,这个词在建筑里就是"建筑物的正面"。你站在外面看,就是一堵漂亮的墙,墙后面是什么?管道、线路、承重结构,全藏起来了。
1.1 为什么需要外观模式?
我在项目中遇到过这样一个场景:一个电商系统的下单流程,涉及库存校验、价格计算、优惠券抵扣、积分更新、订单生成、支付接口调用……十几个子系统来回调用。新来的同事每次改代码都一脸懵,因为调用链太长了。
更麻烦的是,如果某个子系统换了接口,所有调用它的地方都得改。这就是典型的"高耦合"问题。
外观模式要解决的核心问题就是:简化复杂系统的使用,降低客户端与子系统之间的耦合。
核心定义:为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
1.2 外观模式的结构
外观模式的结构其实很简单,就三个角色:
- 外观角色(Facade):客户端调用的统一入口,知道哪些子系统类负责处理哪些请求
- 子系统角色(SubSystem):实现具体功能的模块,可以是一个类,也可以是一组类
- 客户端(Client):通过外观角色访问子系统功能
嗯,这里要注意:外观模式并没有"封装"子系统,它只是提供了一个更友好的接口。子系统类依然可以被客户端直接调用——如果你非要绕开外观的话。
1.3 代码示例:电商下单外观
我们直接看代码。假设有三个子系统:库存、支付、物流。
// 子系统1:库存服务
public class InventoryService {
public boolean checkStock(String productId, int quantity) {
System.out.println("检查库存:" + productId + ",数量:" + quantity);
return true; // 假设库存充足
}
public void deductStock(String productId, int quantity) {
System.out.println("扣减库存:" + productId + ",数量:" + quantity);
}
}
// 子系统2:支付服务
public class PaymentService {
public boolean processPayment(String orderId, double amount) {
System.out.println("处理支付:订单" + orderId + ",金额" + amount);
return true;
}
}
// 子系统3:物流服务
public class ShippingService {
public String createShipping(String orderId, String address) {
System.out.println("创建物流:订单" + orderId + ",地址" + address);
return "SF123456789";
}
}
现在,我们创建一个外观类,把这些乱七八糟的调用封装起来:
// 外观类:订单服务
public class OrderFacade {
private InventoryService inventoryService;
private PaymentService paymentService;
private ShippingService shippingService;
public OrderFacade() {
this.inventoryService = new InventoryService();
this.paymentService = new PaymentService();
this.shippingService = new ShippingService();
}
public String createOrder(String productId, int quantity,
double amount, String address) {
// 1. 检查库存
if (!inventoryService.checkStock(productId, quantity)) {
throw new RuntimeException("库存不足");
}
// 2. 扣减库存
inventoryService.deductStock(productId, quantity);
// 3. 处理支付
String orderId = "ORD" + System.currentTimeMillis();
if (!paymentService.processPayment(orderId, amount)) {
throw new RuntimeException("支付失败");
}
// 4. 创建物流
String trackingNo = shippingService.createShipping(orderId, address);
System.out.println("订单创建成功,物流单号:" + trackingNo);
return trackingNo;
}
}
客户端调用就变得极其简单:
public class Client {
public static void main(String[] args) {
OrderFacade orderFacade = new OrderFacade();
// 一行代码完成下单,背后发生了什么?客户端根本不用关心
orderFacade.createOrder("P001", 2, 299.00, "北京市朝阳区");
}
}
我的经验:外观模式特别适合用在"老系统改造"的场景。我曾经接手一个遗留系统,十几个模块互相调用,改一个功能要排查半天。后来我在每个业务入口加了一层外观,把调用链收拢到外观里。三个月后,新功能开发效率提升了至少40%。
1.4 外观模式的适用场景
| 场景 | 说明 | 示例 |
|---|---|---|
| 复杂系统简化 | 子系统太多,调用链太长 | 电商下单、银行转账 |
| 分层架构 | 为每一层提供统一入口 | Service层对Controller暴露外观 |
| 第三方集成 | 封装第三方SDK的复杂调用 | 支付网关、短信服务 |
| 老系统重构 | 逐步替换旧模块,外观作为过渡层 | 遗留系统现代化改造 |
1.5 外观模式的优缺点
优点:
- 客户端代码变得简洁,不需要了解子系统内部细节
- 降低了客户端与子系统的耦合,子系统变化不影响客户端
- 提供了一个"安全层",可以控制哪些子系统功能对外暴露
缺点:
- 外观类可能变成"上帝类"——什么都知道,什么都管。我见过一个外观类里塞了50多个方法,那已经不是外观了,那是灾难
- 如果外观设计得不好,反而增加了系统的复杂度
- 不能限制客户端直接使用子系统——如果有人非要绕过外观,你也没办法
避坑指南:我曾经在一个项目中,把外观类设计成了"万能入口",所有业务逻辑都往里面塞。结果外观类越来越臃肿,最后不得不拆成三个外观类。记住:外观模式是"简化调用",不是"集中所有逻辑"。
1.6 外观模式 vs 其他模式
很多人会把外观模式和适配器模式搞混。我简单说一下区别:
- 适配器模式:解决"接口不兼容"的问题,把A接口转换成B接口
- 外观模式:解决"接口太复杂"的问题,把多个接口合并成一个简单接口
打个比方:适配器是"转换插头",让你能把两脚插头插到三脚插座上;外观是"遥控器",把电视、空调、音响的操作集中到几个按钮上。
1.7 实际项目中的最佳实践
我个人习惯在外观模式中遵循几个原则:
- 外观类不要包含业务逻辑——它只负责编排调用顺序,真正的逻辑在子系统里
- 一个外观对应一个业务场景——比如下单一个外观,退款一个外观,不要混在一起
- 外观类的方法参数尽量简单——最好只传必要参数,不要传整个对象
- 考虑使用抽象外观——如果子系统可能变化,可以定义接口,让不同外观实现
嗯,最后说一句:外观模式看起来简单,但用好它需要你对系统的边界有清晰的认识。什么时候该封装,什么时候该暴露,这考验的是架构能力,不是编码能力。
总结一句话:外观模式就是给复杂系统装一个"傻瓜式"操作面板,让客户端用起来爽,让子系统改起来自由。
公众号:蓝海资料掘金营,微信 deep3321