我第一次意识到“外观”这种东西。
不是在 GoF。
是在 gcc hello.c 这条命令上。
那会儿我还挺天真。
觉得编译就是“一个程序把 C 代码变成可执行文件”。
后来有一天。
我加了个 -v。
屏幕滚了一屏。
预处理器跑了一遍。
编译器跑了一遍。
汇编器跑了一遍。
链接器又跑了一遍。
我当场就明白了。
原来 gcc 很多时候只是个司机。
它把一堆彼此脾气不太好的工具。
按顺序叫出来。
把参数配好。
把临时文件收拾掉。
然后对你说。
“没事。 你只需要记住我这个入口。”
这就是外观。
它不是新发明。
更像是工程世界里最朴素的一种“礼貌”。
把复杂留给内部。
把简单留给使用者。
你可以让机器复杂。 别让人复杂。
复杂不是罪。
我见过很多子系统。
复杂得很合理。
它可能要和硬件打交道。
要和网络握手。
要和磁盘对账。
要做缓存。
要做回滚。
要做各种“你不做就会死”的边角。
问题出在。
复杂开始向外溢出。
调用方为了完成一件事。
不得不认识一堆类。
不得不记住一堆调用顺序。
不得不在业务里写“初始化姿势”。
你以为你在写业务。
其实你在背一本仪式手册。
很多年后你回头看。
这类代码最大的特点是。
它并不难。
它只是烦。
而且它到处都是。
一个很常见的现场:导出一个 mp4
我拿一个比较“现实”的例子。
多媒体处理。
你只想做一件事。
把 a.mkv 导成 b.mp4。
但你接到的库。
通常不会给你一个 exportMp4()。
它会给你一堆部件。
每个部件都挺专业。
也挺有道理。
然后把“把它们拼起来”这件事。
塞回你手里。
先看没有外观时的写法。
我故意写得像很多项目里真实存在的样子。
Demuxer demux;
Codec codec;
Muxer mux;
demux.open("a.mkv");
codec.init();
mux.open("b.mp4");
auto packet = demux.read();
auto frame = codec.decode(packet);
auto outPacket = codec.encode(frame);
mux.write(outPacket);
mux.close();
这段代码的问题不在“看不懂”。
而在“谁来负责顺序”。
谁来保证 mux.close() 一定会执行。
谁来保证中途失败时该怎么收尾。
谁来保证未来 Codec 多加了一个 warmup()。
所有调用点都不会一起炸。
更要命的是。
这只是一个调用点。
你只要在系统里出现三处。
它就会开始复制。
复制久了。
你就会在凌晨两点。
盯着一个缺少 close() 的分支。
心里默念。
“我当年怎么敢这么写。”
外观的第一份工资:把“拼装”从业务里拿走
外观模式干的事。
说白了。
就是写一个“前台窗口”。
对外只有一个入口。
对内你想怎么折腾都行。
class MediaFacade {
public:
void exportMp4(std::string_view in, std::string_view out) {
demux_.open(in);
codec_.init();
mux_.open(out);
auto packet = demux_.read();
auto frame = codec_.decode(packet);
auto outPacket = codec_.encode(frame);
mux_.write(outPacket);
mux_.close();
}
private:
Demuxer demux_;
Codec codec_;
Muxer mux_;
};
调用方突然就变得像“人写的代码”了。
MediaFacade media;
media.exportMp4("a.mkv", "b.mp4");
你注意到了吗。
外观并没有消灭复杂。
它只是把复杂关进了房间。
把走廊留给了调用方。
这一步在工程里非常值钱。
因为调用方是一大片。
子系统通常只有一小撮维护者。
把复杂放在“小撮”里。
比把复杂撒到“一大片”里。
安全太多。
外观的第二份工资:你终于可以换内部了
外观真正的价值。
往往在半年后才显灵。
最开始你只觉得。
“省了几行代码。”
后来你想换库。
想把 Codec 换成另一个实现。
想加缓存。
想把单线程改成流水线。
你突然发现。
业务侧一个字都不用动。
因为它只认识 exportMp4()。
这就是外观留的退路。
不是为了今天舒服。
是为了明天还能活。
别小看 #include
外观还有一个很“土”的好处。
但我很喜欢。
就是它能减少你在业务里 #include 的数量。
你可以把一堆头文件。
挡在外观背后。
业务里只 #include "media_facade.h"。
这不是洁癖。
这在 C++ 项目里。
经常能直接带来编译时间的下降。
也能减少一堆因为头文件顺序不同。
而出现的“玄学报错”。
你把依赖的传播半径。
收小了。
工程会更稳。
外观不是适配器。
我知道。
你脑子里一定冒出了一个词。
“这不就是包装吗?”
是。
但包装有很多种。
外观和适配器最容易混。
区别不在结构。
在动机。
适配器是在翻译。
两边接口不一样。
你夹在中间当口译。
外观不是翻译。
它是在收拢。
你不是为了“让两边能说话”。
你是为了“让外面少说话”。
外观也经常和代理长得像。
都像一个中间人。
但代理通常在意“控制访问”。
权限。
缓存。
延迟加载。
外观更多在意“简化使用”。
把调用路径变短。
把使用姿势变稳定。
还有一个更微妙的近亲。
中介者 (Mediator)。
它关注的是“对象之间怎么协作”。
外观关注的是“外部怎么用这个子系统”。
一个管内。
一个管外。
你要是把外观写成了“谁都来找它协调”。
它就容易长成上帝对象。
然后你会再次回到那条老路。
“所有改动都要先去拜码头”。
让高手点赞的那句话:外观是边界,不是垃圾桶
外观有一个常见误用。
就是把它写成“万能入口”。
任何需求一来。
先往外观里塞一个方法。
久而久之。
外观变成了整个系统的“总开关面板”。
里面堆满了互相无关的按钮。
这时候你以为你在简化。
其实你在集中风险。
我更喜欢把外观当成边界。
边界的意思是。
外面的人。
只能从这里进来。
但里面怎么分层。
怎么拆模块。
怎么做测试替身。
外面的人不需要知道。
也不应该知道。
所以外观接口要克制。
要稳定。
要像一个老系统里最靠谱的那个同事。
你可以不喜欢他。
但你知道。
找他不会错。
收个尾
外观模式不神秘。
它甚至有点“朴素”。
朴素到你一写出来。
就像是在承认。
“是的。 我这里很复杂。”
但工程就是这样。
复杂不可怕。
可怕的是复杂到处跑。
下次你在项目里看到那种。
初始化要按特定顺序。
收尾要按特定姿势。
调用点像野草一样扩散。
你心里冒出那句冲动。
“我能不能只认识一个类。”
别压着。
那通常是你经验在提醒你。
该给系统立一块前台窗口了。