外观模式:统一接口,子系统封装

外观模式,说白了就是给复杂系统装个「傻瓜式」操作面板。

我刚开始做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, "数据库连接超时");

嗯,这才像话。一行代码搞定所有事情。

外观模式的结构图

我画了一张图,帮你理清各个角色的关系:

外观模式结构图 客户端 (Client) LogFacade (外观类) log() flush() init() FileLogger 文件写入 LogFormatter 格式化 LogBuffer 缓冲管理 LogFilter 日志过滤 ConsoleLogger 控制台输出 RemoteLogger 远程发送

外观模式的优缺点

优点 缺点
客户端代码简洁,无需了解子系统细节 外观类可能成为「上帝对象」,职责过重
降低耦合,子系统变化不影响客户端 不符合开闭原则,修改外观可能影响所有客户端
便于分层,提供清晰的调用入口 过度封装可能隐藏必要的灵活性
编译依赖减少,加快构建速度 调试时可能需要穿透外观层
我曾经踩过的坑:

有一次我把所有功能都塞进外观类,结果这个类膨胀到3000多行。后来重构时,我拆成了多个小外观——比如 LogWriteFacade 负责写入,LogConfigFacade 负责配置。记住:外观类不是垃圾桶,别什么都往里扔

什么时候用外观模式?

我个人习惯在以下场景使用:

  • 子系统太复杂:比如有十几个类互相依赖,新人上手困难
  • 需要分层架构:每一层对外只暴露一个入口
  • 第三方库封装:比如封装一个日志库、网络库,方便后续替换
  • 性能敏感场景:外观层可以做批量操作、缓存、延迟初始化
小技巧:

外观类不一定非得是单例。我见过很多项目把 LogFacade 做成单例,结果单元测试时 mock 不了。建议用依赖注入,或者至少提供 setInstance() 方法用于测试。

外观模式 vs 其他模式

有朋友问我:外观模式和适配器模式有什么区别?

我打个比方:适配器是「转换接口」——比如你把 USB-C 转成 HDMI,接口变了但功能没变。外观是「简化接口」——比如你把整个家庭影院系统简化成一个「看电影」按钮,按一下自动关灯、放碟、调音量。

适配器解决的是不兼容问题,外观解决的是太复杂问题。两者可以结合使用,但别搞混。

写在最后

外观模式是我在项目中用得最多的模式之一。它不炫技,但很实用。你想想看,一个系统里如果每个模块都直接操作底层细节,那代码就像一团乱麻。外观模式就是那把「梳子」,把乱麻理顺了,露出一个整洁的表面。

嗯,下次你写日志系统、配置系统、或者任何需要对外提供简洁接口的模块,不妨试试外观模式。记住:对外简单,对内强大,这才是好的设计。


公众号:蓝海资料掘金营,微信deep3321