设计模式与模块化:外观模式封装模块、中介者协调模块、观察者通知模块
模块化设计,说白了就是拆。但拆完之后怎么让这些模块好好协作,才是真正考验功底的地方。我这些年做系统重构,见过太多拆得稀碎、最后耦合成一团乱麻的项目。
今天聊的三个模式——外观、中介者、观察者——正好对应模块化中的三个核心问题:怎么封装模块、怎么协调模块、怎么通知模块。你想想看,这三个问题解决了,模块化才算真正落地。
一、外观模式:给模块一个干净的门面
外观模式,我习惯叫它"门面模式"。它的核心思想很简单:给复杂子系统提供一个统一的高层接口。
我在项目中遇到过这样一个场景:一个支付模块,内部有账户校验、风控检查、渠道路由、日志记录等七八个步骤。调用方每次都要依次调用这些接口,顺序还不能错。结果呢?调用方代码里全是if-else,改一个步骤顺序,所有调用方都得改。
外观模式就是来解决这个问题的。你想想看,调用方其实只关心"支付成功"这个结果,至于内部怎么校验、怎么路由,那是模块自己的事。
// Java 示例:支付模块外观
public class PaymentFacade {
private AccountValidator accountValidator;
private RiskController riskController;
private ChannelRouter channelRouter;
private AuditLogger auditLogger;
public PaymentResult pay(PaymentRequest request) {
// 封装内部复杂流程
if (!accountValidator.validate(request.getAccount())) {
return PaymentResult.fail("账户校验失败");
}
if (!riskController.check(request)) {
return PaymentResult.fail("风控拦截");
}
Channel channel = channelRouter.route(request);
PaymentResult result = channel.pay(request);
auditLogger.log(request, result);
return result;
}
}
核心要点:外观模式不是把多个接口合并成一个,而是把调用顺序、异常处理、结果组装这些逻辑封装在门面里。调用方只依赖门面,不依赖内部任何子模块。
我的经验:外观模式特别适合做模块的"入口"。我一般会在每个模块的顶层包下放一个Facade类,所有外部调用只能通过这个类。这样内部怎么重构,外部都不受影响。
二、中介者模式:让模块之间不直接说话
模块之间直接通信,看起来效率高,但耦合度也高。A调B,B调C,C又调A,最后形成一个网状结构。改一个模块,牵一发而动全身。
中介者模式的做法是:所有模块只跟中介者通信,模块之间不直接交互。中介者负责协调各个模块的行为。
我曾经接手过一个聊天室系统,每个用户加入、离开、发消息,都要通知其他所有用户。代码里到处都是for循环遍历用户列表,然后挨个调用。后来改成中介者模式,聊天室本身作为中介者,用户只跟聊天室交互,清爽多了。
// C++ 示例:聊天室中介者
class ChatRoomMediator {
public:
virtual void sendMessage(User* sender, const std::string& msg) = 0;
virtual void addUser(User* user) = 0;
virtual void removeUser(User* user) = 0;
};
class ChatRoom : public ChatRoomMediator {
private:
std::vector<User*> users;
public:
void sendMessage(User* sender, const std::string& msg) override {
for (auto user : users) {
if (user != sender) {
user->receive(sender->getName(), msg);
}
}
}
void addUser(User* user) override { users.push_back(user); }
void removeUser(User* user) override { /* 移除逻辑 */ }
};
避坑指南:我曾经犯过一个错误——把中介者搞成了"上帝对象",所有业务逻辑都塞进去。结果中介者比所有模块加起来还复杂。记住:中介者只负责协调,不负责执行。具体的业务逻辑还是放在各个模块里。
三、观察者模式:模块间的"广播"机制
观察者模式解决的是"一对多"的通知问题。一个模块状态变了,需要通知多个其他模块。最典型的例子就是事件驱动架构。
我习惯把观察者模式理解为"订阅-发布"。发布者不关心谁订阅了,订阅者也不关心谁发布了。双方只通过事件类型来匹配。这种松耦合,在模块化设计中非常实用。
// Java 示例:订单状态变更通知
public class OrderEventBus {
private Map<String, List<OrderObserver>> observers = new HashMap<>();
public void subscribe(String eventType, OrderObserver observer) {
observers.computeIfAbsent(eventType, k -> new ArrayList<>()).add(observer);
}
public void publish(String eventType, Order order) {
List<OrderObserver> list = observers.get(eventType);
if (list != null) {
for (OrderObserver observer : list) {
observer.onOrderChanged(order);
}
}
}
}
// 使用示例
OrderEventBus eventBus = new OrderEventBus();
eventBus.subscribe("ORDER_PAID", new InventoryUpdater()); // 库存更新
eventBus.subscribe("ORDER_PAID", new EmailNotifier()); // 邮件通知
eventBus.subscribe("ORDER_PAID", new LogisticsDispatcher()); // 物流调度
关键区别:中介者模式是点对点协调,观察者模式是一对多广播。中介者知道谁在跟谁通信,观察者模式中发布者根本不知道谁在监听。这两个模式不冲突,经常一起用。
四、三个模式如何协同工作
在实际项目中,这三个模式经常配合使用。我画了一张图来说明它们的关系:
从这张图可以看出:
- 外观模式负责每个模块的"对外接口",让模块内部实现对外部透明
- 中介者模式负责模块之间的"协调",避免模块直接耦合
- 观察者模式负责模块之间的"通知",实现松耦合的事件驱动
五、实战中的组合用法
我记得在一个电商系统中,我们是这样组合使用的:
| 场景 | 使用的模式 | 说明 |
|---|---|---|
| 订单模块对外接口 | 外观模式 | OrderFacade 封装了创建、支付、退款等流程 |
| 订单与库存的协调 | 中介者模式 | OrderMediator 协调订单状态变更和库存扣减 |
| 订单状态变更通知 | 观察者模式 | 订单状态变更后,通知物流、财务、客服等模块 |
我的建议:不要为了用模式而用模式。先画出模块之间的依赖关系,如果发现网状依赖,就用中介者拆开;如果发现调用链太长,就用外观封装;如果发现一对多通知,就用观察者。模式是工具,不是目的。
六、避坑指南
这些年踩过的坑,分享几个给你:
- 外观模式不要变成"上帝类"——我曾经把整个业务逻辑都塞进Facade,结果Facade比内部模块还复杂。外观只做"编排",不做"业务"。
- 中介者不要处理所有事情——中介者只负责"协调",具体的"执行"还是交给各个模块。否则中介者会变成性能瓶颈和单点故障。
- 观察者要注意内存泄漏——订阅了不取消,对象就永远无法被回收。我见过一个系统因为观察者没取消订阅,内存涨了三天直接OOM。
- 三个模式可以共存——它们不是互斥的。外观封装模块边界,中介者协调模块交互,观察者广播状态变更。各司其职,互不冲突。
嗯,模块化设计说到底就是"高内聚、低耦合"这六个字。外观、中介者、观察者这三个模式,正好从不同角度帮你实现这个目标。下次做模块化设计的时候,不妨想想:我的模块边界清晰吗?模块之间怎么协调?状态变更怎么通知?这三个问题想清楚了,代码质量自然就上去了。
公众号:蓝海资料掘金营,微信deep3321