设计模式与生活③:你的早晨,是一套模板

设计模式与生活③:你的早晨,是一套模板

Scroll Down

周一早上,闹钟响的时候,天还没亮透。

小王眼睛都没睁开,伸手把闹钟按掉,脑子里已经把今天早上要干的事过了一遍:

起床 → 洗漱 → 做早餐 → 骑电动车到地铁站 → 坐地铁 → 到公司打卡

归拢成大类,其实是五步:起床、洗漱、早餐、通勤、打卡——骑车和地铁,都算通勤那一步里的事。

一步都不能乱。不洗脸出门,地铁上自己难受;不骑车,赶不上那班车;早饭不吃,上午十点手就抖。

这套流程他跑了两年,顺序一次没变过。

scene-morning

眼睛还没睁开,锅已经热了

有意思的是,同组的小李流程一模一样,做法全不一样。小李的早饭是路过便利店顺手买的饭团,不骑车,开车直接到公司楼下。

同一套流程,两个人两种活法。

今天聊的,就是这件事在代码里的写法:模板方法(Template Method)——把流程的骨架定死,把每一步怎么做,留给别人填。系列第三篇。

01 · 先把流程抄三遍

先看一个不太聪明的写法。假设你们组十个人,每个人把早晨写成一个类。

// 小王的早晨
class WangMorning {
    void run() {
        wakeUp(); wash();
        makeToast();        // 小王:煎蛋 + 吐司
        rideToSubway();     // 小王:电动车到地铁站
        takeSubway();       // 小王:坐地铁
        clockIn();
    }
}

// 小李的早晨
class LiMorning {
    void run() {
        wakeUp(); wash();
        buyOnigiri();       // 小李:顺手买的饭团
        driveToWork();      // 小李:开车直达
        clockIn();          // ← 通勤改了,这行照样得抄一遍
    }
}

十个人,十份一模一样的骨架

麻烦的不是抄,是改。

公司把打卡时间提前半小时,或者流程里加一步"出门前带工牌"——十个人,十个类,十处要改。漏掉一个,那个人第二天就被记迟到。

更要命的是,你没法保证十份骨架真的一样。有人把 wash() 写成刷个牙就算洗过;有人干脆忘了 clockIn()。

流程这种东西,只要允许复制,就一定会有副本走样。

02 · 骨架写一遍,空留给子类

模板方法的解法只有一句话:骨架只写一遍,放进父类;每一步怎么做,留个空,让子类填。

abstract class MorningRoutine {

    // 骨架:顺序定死,子类不许改
    public final void run() {
        wakeUp();
        wash();
        breakfast();   // ← 留空
        commute();     // ← 留空
        clockIn();
    }

    private void wakeUp()  { System.out.println("起床,摸袜子"); }
    private void wash()    { System.out.println("刷牙洗脸"); }
    private void clockIn() { System.out.println("打卡"); }

    // 这两个空,交给子类
    protected abstract void breakfast();
    protected abstract void commute();
}

run() 是 final:顺序定死,子类只填空

子类就只剩两个空要填:

class WangMorning extends MorningRoutine {
    protected void breakfast() { System.out.println("煎蛋 + 吐司"); }
    protected void commute()   { System.out.println("电动车到地铁站,再坐地铁"); }
}

class LiMorning extends MorningRoutine {
    protected void breakfast() { System.out.println("便利店饭团"); }
    protected void commute()   { System.out.println("开车直达公司"); }
}

骨架一行没抄,做法各写各的

template

左边:各抄各的,改一处处处改;右边:骨架一遍,空各填各的

这里有两个地方是灵魂,值得停一下。

第一个是 run() 上的 final。少了这一句,子类就能连顺序一起改,模板方法立刻退化成"一堆随时可以推翻的方法"。

第二个是 private 和 abstract 的分工:起床、洗漱、打卡,父类自己干(private,子类碰不到);早餐、通勤,父类不知道你会怎么干(abstract,子类必须给答案)。

routine

灰色三步父类写死,红色两步由子类填空

这种"父类定流程、回头来调子类"的关系,圈里给它起了个名字,叫好莱坞原则:别来找我,我会找你。

生活里就是这样——不是你决定什么时候吃早饭,是流程走到那一步,轮到你了。

03 · 留个钩子:这一步做不做,你说了算

现实里还有一种情况:流程里有一步,不是人人都有。

周五公司允许居家办公。那天小王不用出门,通勤这步直接跳过。

但骨架是写死的,跳过也得有个正经说法。模板方法的做法是留一个"钩子":

public final void run() {
    wakeUp();
    wash();
    breakfast();
    if (needCommute()) {   // ← 钩子:默认要通勤
        commute();
    }
    clockIn();
}

// 钩子方法:父类先给个默认答案
protected boolean needCommute() { return true; }

// 居家那天:不走,也不用你走
class WfhMorning extends MorningRoutine {
    protected boolean needCommute() { return false; }
    protected void breakfast() { System.out.println("慢慢煮碗面"); }
    protected void commute()   { /* 今天不执行,但照样得填 */ }
}

钩子:父类先答了,子类可以改口

钩子和抽象方法的区别,一句话:抽象方法是"必须填的空",钩子是"父类先答了,你可以改口"。

这里还藏着一个真实得有点好笑的细节:居家那天你压根不走路,可 commute() 是 abstract,照样得写个空方法摆在那儿。框架让你填空,不让你把题撕了。

04 · 这套写法,你天天在碰

  • 🖥️ HttpServlet:service() 把"判断这是 GET 还是 POST"整套流程写死,然后把 doGet() / doPost() 留给你。你只填方法,从不用管请求怎么分派。
  • 🗄️ Spring 的 JdbcTemplate:拿连接、执行 SQL、关连接、把数据库异常翻译成 Spring 自己的异常——这套骨架它管;你只填"查出来的一行怎么变成对象"。
  • 📱 Android 的 Activity:onCreate → onStart → onResume,顺序系统定,你只在回调里填自己的代码。写得再乱,系统也按这个顺序叫你。
  • 🔢 排序:Arrays.sort 把排序算法写死,两个元素谁大谁小,交给你(Comparable.compareTo)。

05 · 什么时候,别把人按进模具

模板方法的软肋,也长在"骨架写死"上。

它靠继承。继承在编译期就绑死了:父类骨架一改,所有子类跟着重编、跟着受影响。而且 Java 只能单继承——MorningRoutine 占了这个位置,就别想再继承别的。

更现实的问题是:你和小李的流程,真的一样吗?

只是"同一套流程换做法",套模板,天经地义。

可要是流程本身就不一样——有人习惯先到公司楼下吃完再上楼,有人到工位才吃第一顿——那就不是填空,是两套流程。硬塞进同一个骨架,等于把人按进模具。

判断标准就一条:差别在步骤内部,还是在步骤之间?

步骤内部不一样 → 模板方法正合适;连步骤和顺序都不一样 → 别套,那得靠组合,不是继承。

06 · 流程是同一个,日子各过各的

回到周一早上。

小王骑着电动车往地铁站赶的时候,小李可能正堵在高架上。两个人的流程一模一样,五步,一步不多一步不少。

流程是同一个,日子各过各的。

这大概是模板方法最像生活的地方:它不规定你早饭吃什么,只规定你必须先刷牙。

顺带跟前面两篇对一句:工厂管"对象怎么造",单例管"造几个",模板方法管"事情怎么走"。前两个管生,这个管走。

带走一句话:模板方法 = 把流程锁进父类(final 的 run),把细节留给子类(abstract 的填空)。

骨架归骨架,做法归做法。

参考资料

  1. Erich Gamma 等,《设计模式:可复用面向对象软件的基础》(GoF 原著)——模板方法的原始定义与"好莱坞原则"
  2. 《Head First 设计模式》第 8 章——用咖啡和茶的冲泡流程讲模板方法,本文"早晨流程"类比的同乡
  3. Spring 官方文档 · JdbcTemplate——回调接口(RowMapper)与模板骨架的分工
  4. Android 官方文档 · Activity 生命周期