外观模式(Facade):子系统角色、外观角色、统一接口封装
外观模式,说白了就是「把复杂的东西藏起来,给外面一个简单的门面」。我刚开始做架构设计那会儿,总觉得把所有接口都暴露出去才叫灵活,结果被调用方吐槽「你们这个系统接口也太多了吧」——嗯,后来我才明白,有时候少即是多。
什么是外观模式?
外观模式的核心思想很简单:为子系统中的一组接口提供一个统一的、更高级的接口。它让子系统更容易使用,也降低了客户端与子系统之间的耦合度。
你想想看,一个复杂的业务系统,内部可能有十几个模块互相调用。如果每个外部调用者都要了解这些模块的细节,那学习成本得多高?外观模式就是给这些复杂操作包一层「皮」,对外只暴露几个核心方法。
核心角色:
- 外观角色(Facade):对外提供统一接口,内部协调各个子系统
- 子系统角色(Subsystem):实现具体功能,但对外部不可见
- 客户端(Client):只与外观角色交互,不直接调用子系统
一个真实的例子:订单系统
我在做一个电商项目时,遇到过这样的场景:用户下单后,系统需要做库存扣减、积分更新、物流通知、短信发送……如果让前端直接调用这四个服务,那代码会写成什么样?
更麻烦的是,哪天业务变了,下单后还要加个「优惠券核销」,前端又得改代码。这不合理。
我们用外观模式重构了一下:
// 子系统:库存服务
class InventoryService {
public void deduct(String productId, int quantity) {
System.out.println("扣减库存: " + productId + ", 数量: " + quantity);
}
}
// 子系统:积分服务
class PointService {
public void add(String userId, int points) {
System.out.println("增加积分: " + userId + ", 积分: " + points);
}
}
// 子系统:物流服务
class LogisticsService {
public void createOrder(String orderId, String address) {
System.out.println("创建物流单: " + orderId + ", 地址: " + address);
}
}
// 外观角色:订单门面
class OrderFacade {
private InventoryService inventory;
private PointService point;
private LogisticsService logistics;
public OrderFacade() {
this.inventory = new InventoryService();
this.point = new PointService();
this.logistics = new LogisticsService();
}
public void placeOrder(String userId, String productId, int quantity, String address) {
// 统一编排各个子系统的调用顺序
inventory.deduct(productId, quantity);
point.add(userId, quantity * 10);
logistics.createOrder("ORD" + System.currentTimeMillis(), address);
System.out.println("下单完成!");
}
}
// 客户端
public class Client {
public static void main(String[] args) {
OrderFacade facade = new OrderFacade();
facade.placeOrder("user001", "P1001", 2, "北京市朝阳区");
}
}
你看,客户端只需要调用一个 placeOrder 方法,背后发生了什么它完全不用管。这就是外观模式的价值。
外观模式与三层架构的Service层
说到这儿,我想聊聊三层架构中的 Service 层设计。很多新手会把 Service 写成「Controller 的直接映射」——Controller 调什么,Service 就暴露什么。这其实是个坑。
我个人习惯把 Service 层当作「业务外观层」。它不应该只是透传 DAO 的接口,而应该封装完整的业务逻辑。比如:
// 不好的设计:Service 只是 DAO 的代理
public class OrderServiceBad {
private OrderDao orderDao;
public Order getOrderById(Long id) {
return orderDao.findById(id);
}
public void saveOrder(Order order) {
orderDao.save(order);
}
}
// 好的设计:Service 作为外观,封装完整业务流程
public class OrderServiceGood {
private OrderDao orderDao;
private InventoryDao inventoryDao;
private PaymentClient paymentClient;
public OrderResult createOrder(CreateOrderRequest request) {
// 1. 校验库存
if (!inventoryDao.hasStock(request.getProductId(), request.getQuantity())) {
return OrderResult.fail("库存不足");
}
// 2. 创建订单
Order order = new Order(request);
orderDao.save(order);
// 3. 发起支付
PaymentResult payment = paymentClient.pay(order);
// 4. 返回结果
return OrderResult.success(order, payment);
}
}
我的经验:Service 层的每个方法,都应该对应一个完整的「业务用例」。如果一个 Service 方法需要前端调三次才能完成一件事,那说明外观封装得不够彻底。
外观模式的适用场景
什么时候该用外观模式?我总结了几个场景:
- 子系统越来越复杂:当你的系统有十几个模块,调用方需要了解每个模块的接口时
- 需要分层解耦:比如三层架构中,Service 层对 Controller 层隐藏 DAO 和外部服务的细节
- 遗留系统集成:老系统接口混乱,但又不能大改,包一层外观是最稳妥的方案
- 性能优化:外观层可以做缓存、批量调用等优化,对客户端透明
注意:外观模式不是万能的。我曾经在一个项目里把外观层做得太「厚」,结果所有业务逻辑都堆在 Facade 里,子系统反而变成了贫血模型。外观层应该只做「编排」和「协调」,不要把业务逻辑都塞进去。
外观模式的核心逻辑图
下面这张图展示了外观模式的结构和调用关系:
外观模式的优缺点
| 优点 | 缺点 |
|---|---|
| 简化调用:客户端只需记住一个接口 | 外观层可能变成「上帝对象」,职责过重 |
| 降低耦合:客户端与子系统解耦 | 增加一层抽象,可能影响性能 |
| 提高安全性:子系统细节对外隐藏 | 外观修改可能影响所有客户端 |
| 便于分层:符合「高内聚低耦合」原则 | 过度封装会降低灵活性 |
避坑指南:我曾经在一个项目中,把外观模式用在了「每个 DAO 方法都包一层 Service」——结果 Service 层变成了纯粹的透传层,毫无价值。外观模式的核心是「封装业务流程」,不是「封装每个原子操作」。记住:外观层应该提供「业务价值」,而不是「技术映射」。
外观模式与适配器模式的区别
很多初学者会把外观模式和适配器模式搞混。我简单说一下:
- 适配器模式:解决「接口不兼容」的问题,把 A 接口转换成 B 接口
- 外观模式:解决「接口太多太复杂」的问题,提供更简单的统一接口
说白了,适配器是「翻译官」,外观是「前台接待」。两者目的不同,不要混用。
总结
外观模式是我在实际项目中使用频率最高的设计模式之一。尤其是在三层架构中,Service 层天然就是外观模式的最佳实践场所。我个人习惯在 Service 层做三件事:
- 封装业务流程:一个 Service 方法对应一个完整的业务用例
- 协调子系统:内部调用 DAO、外部服务、消息队列等
- 提供统一入口:对外暴露简洁的接口,隐藏实现细节
记住一句话:好的外观模式,让调用方「用脚就能写代码」。如果你的 Service 接口需要调用方看文档才能用,那说明外观封装得还不够。
公众号:蓝海资料掘金营,微信deep3321