欢迎光临
我们一直在努力

模板方法模式详解:父类定骨架、子类填细节(含可运行代码 + 钩子方法 + 与策略辨析)

一、什么是模板方法模式

定义(GOF):定义一个操作中的算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变算法结构即可重定义该算法的某些特定步骤。

一句话理解:流程(先做头、再做身、最后做尾)写在父类且不可被改,具体每一步怎么做由子类实现。父类掌控"顺序",子类只管"细节"。


二、为什么需要模板方法模式(动机)

假设有多款车要造,每款车的组装顺序都是 车头 → 车身 → 车尾(流程一致),只是各部件的具体做法不同。

不用模板方法:

  • 每个 MakeXxx 类都各自写一遍完整 make(),流程代码大量重复(违背 DRY)。
  • 流程顺序是"散落的",某天要统一改成"车头→车尾→车身",得改 N 个类,违反开闭原则、极易漏改。

用模板方法:把"车头→车身→车尾"这个不变的顺序提炼到父类的 final make() 里,子类只实现 makeHead/makeBody/makeTail 的细节。流程只在一处定义,加新车型只需新增一个子类。


三、模板方法模式的结构

角色和职责

角色职责
AbstractClass(抽象类) 定义模板方法 make()(算法骨架,通常声明为 final,防止子类破坏顺序);声明若干抽象步骤方法(子类必须实现);可提供具体方法和钩子方法
ConcreteClass(具体子类) 实现父类的抽象步骤方法,定义各步骤的具体细节

类结构图(mermaid)

#mermaid-svg-lXYuXspxgTwj1gnY{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lXYuXspxgTwj1gnY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lXYuXspxgTwj1gnY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lXYuXspxgTwj1gnY .error-icon{fill:#552222;}#mermaid-svg-lXYuXspxgTwj1gnY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lXYuXspxgTwj1gnY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lXYuXspxgTwj1gnY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lXYuXspxgTwj1gnY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lXYuXspxgTwj1gnY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lXYuXspxgTwj1gnY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lXYuXspxgTwj1gnY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lXYuXspxgTwj1gnY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lXYuXspxgTwj1gnY .marker.cross{stroke:#333333;}#mermaid-svg-lXYuXspxgTwj1gnY svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lXYuXspxgTwj1gnY p{margin:0;}#mermaid-svg-lXYuXspxgTwj1gnY g.classGroup text{fill:#9370DB;stroke:none;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-lXYuXspxgTwj1gnY g.classGroup text .title{font-weight:bolder;}#mermaid-svg-lXYuXspxgTwj1gnY .cluster-label text{fill:#333;}#mermaid-svg-lXYuXspxgTwj1gnY .cluster-label span{color:#333;}#mermaid-svg-lXYuXspxgTwj1gnY .cluster-label span p{background-color:transparent;}#mermaid-svg-lXYuXspxgTwj1gnY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lXYuXspxgTwj1gnY .cluster text{fill:#333;}#mermaid-svg-lXYuXspxgTwj1gnY .cluster span{color:#333;}#mermaid-svg-lXYuXspxgTwj1gnY .nodeLabel,#mermaid-svg-lXYuXspxgTwj1gnY .edgeLabel{color:#131300;}#mermaid-svg-lXYuXspxgTwj1gnY .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-lXYuXspxgTwj1gnY .label text{fill:#131300;}#mermaid-svg-lXYuXspxgTwj1gnY .labelBkg{background:#ECECFF;}#mermaid-svg-lXYuXspxgTwj1gnY .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-lXYuXspxgTwj1gnY .classTitle{font-weight:bolder;}#mermaid-svg-lXYuXspxgTwj1gnY .node rect,#mermaid-svg-lXYuXspxgTwj1gnY .node circle,#mermaid-svg-lXYuXspxgTwj1gnY .node ellipse,#mermaid-svg-lXYuXspxgTwj1gnY .node polygon,#mermaid-svg-lXYuXspxgTwj1gnY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lXYuXspxgTwj1gnY .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-lXYuXspxgTwj1gnY g.clickable{cursor:pointer;}#mermaid-svg-lXYuXspxgTwj1gnY g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-lXYuXspxgTwj1gnY g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-lXYuXspxgTwj1gnY .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-lXYuXspxgTwj1gnY .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-lXYuXspxgTwj1gnY .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-lXYuXspxgTwj1gnY .dashed-line{stroke-dasharray:3;}#mermaid-svg-lXYuXspxgTwj1gnY .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-lXYuXspxgTwj1gnY #compositionStart,#mermaid-svg-lXYuXspxgTwj1gnY .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-lXYuXspxgTwj1gnY #compositionEnd,#mermaid-svg-lXYuXspxgTwj1gnY .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-lXYuXspxgTwj1gnY #dependencyStart,#mermaid-svg-lXYuXspxgTwj1gnY .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-lXYuXspxgTwj1gnY #dependencyStart,#mermaid-svg-lXYuXspxgTwj1gnY .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-lXYuXspxgTwj1gnY #extensionStart,#mermaid-svg-lXYuXspxgTwj1gnY .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-lXYuXspxgTwj1gnY #extensionEnd,#mermaid-svg-lXYuXspxgTwj1gnY .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-lXYuXspxgTwj1gnY #aggregationStart,#mermaid-svg-lXYuXspxgTwj1gnY .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-lXYuXspxgTwj1gnY #aggregationEnd,#mermaid-svg-lXYuXspxgTwj1gnY .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-lXYuXspxgTwj1gnY #lollipopStart,#mermaid-svg-lXYuXspxgTwj1gnY .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-lXYuXspxgTwj1gnY #lollipopEnd,#mermaid-svg-lXYuXspxgTwj1gnY .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-lXYuXspxgTwj1gnY .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-lXYuXspxgTwj1gnY .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lXYuXspxgTwj1gnY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lXYuXspxgTwj1gnY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lXYuXspxgTwj1gnY :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

«abstract»

MakeCar

#makeHead()

#makeBody()

#makeTail()

+isAlarm() : : boolean

+makeAlarm()

+make() : final

MakeBus

+makeHead()

+makeBody()

+makeTail()

MakeJeep

+makeHead()

+makeBody()

+makeTail()

+isAlarm() : : boolean

make() 用 final 锁死流程\\n子类禁止重写


四、代码举例(修正反模式)

// ========== 1. 抽象类:定义骨架 ==========
public abstract class MakeCar {
// 抽象步骤:子类必须实现细节
public abstract void makeHead();
public abstract void makeBody();
public abstract void makeTail();

// 钩子方法(Hook):默认不装警报,子类可重写来控制流程分支
public boolean isAlarm() {
return false;
}
public void makeAlarm() {
System.out.println("组装警报器");
}

// 模板方法:算法骨架,final 防止子类破坏顺序
public final void make() {
this.makeHead();
this.makeBody();
this.makeTail();
if (isAlarm()) { // 钩子控制:是否执行某步
this.makeAlarm();
}
}
}

// ========== 2. 具体子类:公交车(不装警报) ==========
public class MakeBus extends MakeCar {
@Override
public void makeHead() { System.out.println("Bus:组装车头"); }
@Override
public void makeBody() { System.out.println("Bus:组装车身"); }
@Override
public void makeTail() { System.out.println("Bus:组装车尾"); }
// 不重写 isAlarm() → 默认 false,无警报步骤
}

// ========== 2. 具体子类:吉普车(装警报) ==========
public class MakeJeep extends MakeCar {
@Override
public void makeHead() { System.out.println("Jeep:组装车头"); }
@Override
public void makeBody() { System.out.println("Jeep:组装车身"); }
@Override
public void makeTail() { System.out.println("Jeep:组装车尾"); }

@Override
public boolean isAlarm() { return true; } // 钩子打开:装警报
}

// ========== 3. 测试 ==========
public class MainClass {
public static void main(String[] args) {
MakeCar bus = new MakeBus();
bus.make();

MakeCar jeep = new MakeJeep();
jeep.make();
}
}

运行结果

Bus:组装车头
Bus:组装车身
Bus:组装车尾
Jeep:组装车头
Jeep:组装车身
Jeep:组装车尾
Jeep:组装警报器

可以看到:流程完全由父类 make() 掌控;Jeep 通过重写钩子 isAlarm() 就让算法"多走了一步",而没有碰过 make() 本身——这就是模板方法的反向控制。


五、进阶:钩子方法(Hook)与好莱坞原则

钩子方法是模板方法模式的"可选步骤"机制:父类在骨架里留一个默认实现(甚至空实现)的方法调用,子类按需重写来影响算法行为(如上面的 isAlarm())。它和"抽象步骤"的区别在于:抽象步骤子类必须实现,钩子子类可选重写。

由此引出模板方法的著名设计原则——好莱坞原则(Hollywood Principle):

“Don’t call us, we’ll call you.”(别打电话给我们,我们会打给你。)

含义:子类不要主动调用父类的流程方法去驱动算法,而是父类在合适的时机反向调用子类实现的细节方法。父类掌控全局,子类只被动"被调用"。这避免了子类破坏整体流程,也是框架设计的惯用套路(框架调你的代码,而不是你调框架的流程)。


六、模板方法 vs 策略模式(极易混淆)

两者都解决了"算法变化",但机制完全不同:

维度模板方法模式策略模式
复用算法的方式 继承(父类定骨架,子类填细节) 组合(持有一个策略对象)
算法结构 骨架固定,仅细节可变 整个算法可整体替换
耦合度 子类与父类强耦合(继承关系) 客户端与策略松耦合(面向接口)
运行时切换 编译期由子类决定,一般不可热切换 可运行时动态换算法
一句话区别 “我(父类)定流程,你(子类)填坑” “我(上下文)持有一张牌,随时换打法”

记忆口诀:流程不变用模板方法(继承),整盘算法要换用策略(组合)。


七、真实世界中的模板方法(JDK / 框架)

  • javax.servlet.HttpServlet:service(HttpServletRequest, HttpServletResponse) 是模板方法,内部根据请求方法分派到 doGet/doPost/doPut 等(这些就是子类填的"步骤")。你写 Servlet 从不需要重写 service(),只重写 doGet/doPost——典型的模板方法。
  • Spring JdbcTemplate:execute() / query() 是模板方法,把"获取连接→创建语句→执行→异常处理→关闭资源"的骨架锁死,把"如何处理 ResultSet"通过回调(RowMapper/PreparedStatementSetter)交给你填。
  • java.io.InputStream:read(byte[] b) 是模板方法,内部循环调用抽象的 read() 单字节读取。
  • java.util.AbstractList / AbstractSet:addAll、iterator 等用抽象方法 get/set/size 拼出骨架,具体列表只实现少量抽象方法。
  • JUnit 3:TestCase.runBare() 是模板方法,依次调 setUp() → testXxx() → tearDown()。

八、应用场景与优缺点

适用场景

  • 多个类有相同的操作步骤,只是部分细节不同。
  • 需要固定算法流程、防止子类改乱顺序(用 final 模板方法)。
  • 希望通过钩子让子类有选择地参与流程。

优点

  • 代码复用:公共流程只在父类写一次。
  • 反向控制(好莱坞原则):父类调子类,流程不被破坏。
  • 符合开闭(操作侧):新增子类容易,不改父类骨架。
  • 缺点

  • 父类牵一发动全身:骨架一改,所有子类受影响。
  • 类数量增加:每个变体一个子类。
  • 继承耦合:子类受限于父类设计,灵活性低于策略(组合)。

  • 九、总结

    一句话:模板方法模式 = 父类用 final 锁死算法流程,子类只实现会变的步骤(必要时用钩子微调分支)。

    记忆口诀:

    骨架锁父类,细节交子类;
    流程别乱改,差异才重写;
    钩子控分支,好莱坞反调。

    赞(0)
    未经允许不得转载:171主机测评 » 模板方法模式详解:父类定骨架、子类填细节(含可运行代码 + 钩子方法 + 与策略辨析)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址