设计模式与生活⑨:一碗胡辣汤,用不着认识十几种香料

设计模式与生活⑨:一碗胡辣汤,用不着认识十几种香料

Scroll Down

周一早上,小王是被饿醒的,醒来就想喝胡辣汤。

前几天他刚在网上囤了几样老家的吃食:胡辣汤料、豆腐菜、饸饹面,都是下锅煮一煮就能吃的。味道跟老家店里比还差着一截,但整体是那个意思——对出门在外的人来说,这一口已经很知足了。

公司是弹性打卡,最晚九点半到,算上电车、地铁和步行,他最晚八点十分就得出家门。

真要从零做一碗胡辣汤,胡椒、花椒、八角十几种香料得按比例配,高汤得提前吊,面筋得洗,芡得勾得刚刚好——这一套下来,班也不用上了。

他撕开了一包胡辣汤料。水开下料,粉条、木耳、豆腐皮下锅,咕嘟几分钟,勾个薄芡,出锅点香油。配着水煎包,一碗下肚,出门上班。

scene-hulatang

一包料下锅,十几种香料的事,就不用他操心了

这包料做的事,值得琢磨:香料还是那十几种,工序一道也没少,但小王只跟"料包"这一个东西打交道。配比、顺序、火候的讲究,都收在了包装里面。

今天聊的就是这个思路:外观模式(Facade),也叫门面模式——给一组复杂的子系统,留一个简单统一的入口。系列第九篇。

01 从零做一碗,得先认识多少东西

先看看没有料包的日子。

想喝胡辣汤,你得认识香料架,知道胡椒是灵魂、剩下的各放多少;得认识汤锅,知道什么时候下料、什么时候下配菜;还得认识芡汁,知道勾早了容易泄、勾晚了汤不挂勺。

用代码写出来,大概是这个样子:

// 三个子系统,各自管一摊
class SpiceRack {
    void mix() { System.out.println("香料架:胡椒为主,十几种香料按比例配好"); }
}

class StockPot {
    void boil()   { System.out.println("汤锅:高汤烧开"); }
    void simmer() { System.out.println("汤锅:下配菜,小火咕嘟几分钟"); }
}

class StarchSlurry {
    void thicken() { System.out.println("芡汁:勾薄芡,汤变得浓稠挂勺"); }
}

香料、汤锅、芡汁,各管一摊

做汤的人得把这一整套都学会,按对的顺序挨个调用:

SpiceRack spices = new SpiceRack();
StockPot pot = new StockPot();
StarchSlurry slurry = new StarchSlurry();

pot.boil();          // 先烧汤
spices.mix();        // 再下配好的香料
pot.simmer();        // 下配菜,小火煮
slurry.thicken();    // 最后勾芡,顺序不能乱

每一步都得自己记,顺序错了味道就不对

问题不在某一行代码,在于"先烧汤、再配香料、后勾芡"这份知识,装在每个做汤的人脑子里。家里谁想喝,谁就得从头学一遍;学漏一步,一锅汤就翻车。

02 料包只留一个动作

料包的思路是:把这份知识从人脑子里挪出来,收进一个固定的地方。

class HuLaTangFacade {
    private final SpiceRack spices = new SpiceRack();
    private final StockPot pot = new StockPot();
    private final StarchSlurry slurry = new StarchSlurry();

    void cook() {                 // 对外只留一个动作
        pot.boil();
        spices.mix();
        pot.simmer();
        slurry.thicken();
        System.out.println("出锅,点香油");
    }
}

配比和顺序,都收进 cook() 里

小王这边,只剩一句:

new HuLaTangFacade().cook();     // 他只需要知道这一句

不用认识十几种香料,也能喝上胡辣汤

注意一件事:SpiceRack、StockPot、StarchSlurry 一个都没少,各自的方法也没改。外观模式不是把子系统砍掉,而是在它们前面加了一层,把"谁按什么顺序调用谁"收拢起来。

facade

子系统没变,变的是做汤的人要懂多少

外观模式的价值,不是让系统变简单,而是让"用系统"这件事变简单。

03 复杂没有消失,只是换了个地方

这里有两处得挑明,别让类比骗了。

第一处:真实的料包是个黑盒。比例在工厂里就定死了,你想少放点胡椒,只能自己往碗里想办法。代码里的门面不是黑盒——cook() 的源码就摆在那儿,你随时可以打开看,也可以绕过它:

HuLaTangFacade soup = new HuLaTangFacade();
soup.cook();                        // 省事的走门面

new SpiceRack().mix();              // 内行也可以直接动用子系统

门面是默认的捷径,不是唯一的入口

这正是外观模式和"封装成黑盒"的区别:子系统不知道门面的存在,门面也不该拦着你直接用它。想自己多配一把香料的人,路得给他留着。

第二处:料包并没有把复杂消灭,只是把复杂的活提前在工厂里干完了。代码里也一样——顺序和配比只是从每个调用方脑子里,挪进了 cook() 一个方法里。真出了 bug,比如汤不够味,你还是得钻进 cook() 里面,一步一步查是哪个子系统出了问题。

门面对外挡的是"使用成本",挡不住"排查成本"。 以为有了门面就不用懂子系统,迟早要在一锅不对味的汤面前补课。

04 门面、转接头和中间人

前几篇聊过的模式,跟门面看着有点像,得把边界划清。

代理(Day 7)是"代表某一个人":打车平台挡在你和司机中间,管你能不能碰到真司机,重点是控制访问。门面是"收拢一堆零件":香料架、汤锅、芡汁谁也不被代表,它们照常干活,只是你不用再挨个找它们。

适配器是"换个接口的形状":出国带转换插头,是因为插孔和插头对不上,得转成能插进去的形状。门面不管形状对不对,管的是"接口太多、太碎",把它们合并成少数几个。一句话区分:适配器改接口,代理控访问,外观简接口。

Java 生态里最典型的门面,你可能天天在用:SLF4J,名字就叫 Simple Logging Facade for Java。你代码里只写 Logger 和 log.info(...),底下是 Logback 还是 Log4j2、怎么滚动文件、怎么异步刷盘,都不用你挨个认识。Spring 的 JdbcTemplate 也是一路:获取连接、执行 SQL、处理结果集、关闭资源,这一长串原本要自己按顺序伺候的步骤,收进了 query()、update() 几个方法里。

AI 可以一秒生成一个门面类,但"哪些调用该收进这个入口、哪些该留在外面",它替不了你做主——这取决于你希望别人怎么用这套系统。

05 什么时候,别急着包门面

不是所有东西都值得一个料包。

只调用一个简单对象时,别包。 烧壶白开水,直接按开关就行,犯不上先设计一个"饮水门面",把 press() 改名成 drink()。

别让门面长成上帝类。 门面的本职是编排调用顺序,不是把业务规则也长进去。今天往里塞个"自动配早餐",明天塞个"顺便叫车",最后门面本身成了全家最复杂的地方——那等于把十几种香料又倒回了桌上。

别把门面变成强制关卡。 前面说过,内行要能绕过它。一旦强制所有人只能走门面,想定制的人没法定制,门面就从"捷径"变成了"路障"。

判断标准可以很简单:调用方是不是总在重复同一串样板调用? 是,就值得收进一个入口;不是,直接调用反而更明白。

06 喝完汤,出门上班

小王那碗胡辣汤,从撕料到喝完没花多少工夫,出门、上班,一点没耽误。

香料还是那十几种,工序一道没少,只是这个早晨,它们不必都经过他。

跟前面八篇对一句:单例管造几个,工厂管怎么造,模板方法管事情怎么走,策略管这一步用什么做法,装饰器管外面加几层,命令管把要做的事做成对象,代理管谁能碰到真东西,观察者管状态变了告诉谁,外观管一堆零件只留一个入口。

带走一句话:外观 = 给一堆子系统,留一个简单统一的入口。

复杂没有消失,只是不必再经过你。

愿你周一的这碗汤,也有人替你配好了料。愿出门在外的人,都能用一包料的工夫,煮出一口老家的味道。


参考资料