外观模式:统一接口,子系统封装
外观模式,说白了就是给复杂系统装个「傻瓜式」操作面板。
我刚开始做C++项目时,接手过一个日志系统。那代码写得,啧啧,直接操作文件流、管理缓冲区、处理多线程锁……每次写一行日志,得调用五六个不同的类。新同事来了,光看文档就得看两天。我当时就想:能不能给外面的人一个简单接口?
这就是外观模式的核心思想——你不需要知道内部有多复杂,你只需要一个简单的入口。
什么是外观模式?
外观模式(Facade Pattern)属于结构型设计模式。它为一组复杂的子系统接口提供一个统一的、更高级的接口。说白了,就是封装复杂性,暴露简洁性。
举个例子:你开汽车,不需要知道发动机怎么点火、变速箱怎么换挡、刹车泵怎么增压。你只需要踩油门、踩刹车、打方向盘。外观模式就是那个「方向盘+踏板」的抽象层。
- 外观类(Facade):对外提供统一接口
- 子系统类(Subsystem):内部真正的干活者
- 客户端(Client):只跟外观类打交道
日志系统的痛点
我参与过一个金融交易系统的日志模块。最初的设计是这样的:
// 客户端需要知道这些类
FileLogger fileLogger;
ConsoleLogger consoleLogger;
LogFormatter formatter;
LogFilter filter;
LogBuffer buffer;
// 每次写日志,要手动组合
buffer.append(formatter.format("INFO", "交易完成"));
if (filter.shouldLog("INFO")) {
fileLogger.write(buffer.flush());
consoleLogger.write(buffer.flush());
}
你想想看,每个业务模块都要重复写这段代码。而且一旦日志策略变了——比如要加个远程日志服务器——所有调用处都得改。这维护成本,谁受得了?
用外观模式重构日志系统
我当时做的第一件事:封装一个 LogFacade。
class LogFacade {
public:
enum Level { DEBUG, INFO, WARN, ERROR };
void init(const std::string& configFile) {
// 内部初始化所有子系统
m_formatter = std::make_unique<LogFormatter>(configFile);
m_filter = std::make_unique<LogFilter>(configFile);
m_fileLogger = std::make_unique<FileLogger>(configFile);
m_consoleLogger = std::make_unique<ConsoleLogger>();
m_remoteLogger = std::make_unique<RemoteLogger>(configFile);
m_buffer = std::make_unique<LogBuffer>(1024 * 1024); // 1MB缓冲区
}
void log(Level level, const std::string& message) {
if (!m_filter->shouldLog(level)) return;
std::string formatted = m_formatter->format(level, message);
m_buffer->append(formatted);
// 批量写入,提升性能
if (m_buffer->isFull() || level == ERROR) {
flush();
}
}
void flush() {
std::string data = m_buffer->flush();
m_fileLogger->write(data);
m_consoleLogger->write(data);
m_remoteLogger->write(data);
}
private:
std::unique_ptr<LogFormatter> m_formatter;
std::unique_ptr<LogFilter> m_filter;
std::unique_ptr<FileLogger> m_fileLogger;
std::unique_ptr<ConsoleLogger> m_consoleLogger;
std::unique_ptr<RemoteLogger> m_remoteLogger;
std::unique_ptr<LogBuffer> m_buffer;
};
客户端调用变成了这样:
LogFacade logger;
logger.init("log_config.json");
logger.log(LogFacade::INFO, "用户登录成功");
logger.log(LogFacade::ERROR, "数据库连接超时");
嗯,这才像话。一行代码搞定所有事情。
外观模式的结构图
我画了一张图,帮你理清各个角色的关系:
外观模式的优缺点
| 优点 | 缺点 |
|---|---|
| 客户端代码简洁,无需了解子系统细节 | 外观类可能成为「上帝对象」,职责过重 |
| 降低耦合,子系统变化不影响客户端 | 不符合开闭原则,修改外观可能影响所有客户端 |
| 便于分层,提供清晰的调用入口 | 过度封装可能隐藏必要的灵活性 |
| 编译依赖减少,加快构建速度 | 调试时可能需要穿透外观层 |
有一次我把所有功能都塞进外观类,结果这个类膨胀到3000多行。后来重构时,我拆成了多个小外观——比如 LogWriteFacade 负责写入,LogConfigFacade 负责配置。记住:外观类不是垃圾桶,别什么都往里扔。
什么时候用外观模式?
我个人习惯在以下场景使用:
- 子系统太复杂:比如有十几个类互相依赖,新人上手困难
- 需要分层架构:每一层对外只暴露一个入口
- 第三方库封装:比如封装一个日志库、网络库,方便后续替换
- 性能敏感场景:外观层可以做批量操作、缓存、延迟初始化
外观类不一定非得是单例。我见过很多项目把 LogFacade 做成单例,结果单元测试时 mock 不了。建议用依赖注入,或者至少提供 setInstance() 方法用于测试。
外观模式 vs 其他模式
有朋友问我:外观模式和适配器模式有什么区别?
我打个比方:适配器是「转换接口」——比如你把 USB-C 转成 HDMI,接口变了但功能没变。外观是「简化接口」——比如你把整个家庭影院系统简化成一个「看电影」按钮,按一下自动关灯、放碟、调音量。
适配器解决的是不兼容问题,外观解决的是太复杂问题。两者可以结合使用,但别搞混。
写在最后
外观模式是我在项目中用得最多的模式之一。它不炫技,但很实用。你想想看,一个系统里如果每个模块都直接操作底层细节,那代码就像一团乱麻。外观模式就是那把「梳子」,把乱麻理顺了,露出一个整洁的表面。
嗯,下次你写日志系统、配置系统、或者任何需要对外提供简洁接口的模块,不妨试试外观模式。记住:对外简单,对内强大,这才是好的设计。
公众号:蓝海资料掘金营,微信deep3321