【设计模式精讲】27.模板方法模式(Template Method)

【设计模式精讲】27.模板方法模式(Template Method)

【摘要】:三个后台任务类各自手写同一套仪式------初始化、循环、异常捕获、收尾统计,三份代码长得像又各缺一角:有的忘了 try,有的收尾顺序不对,加「统一超时」要改三处将来 N 处。本文从仪式的复制讲起,给出模板方法的 GoF 意图:在基类固化算法骨架,把可变步骤延迟到子类------骨架非虚、钩子分「必填纯虚」与「可选默认」两档;C++ 特有的 NVI 惯用法(公有非虚 + 私有虚)与模板方法互为表里,现代版补上 final、函数注入版(骨架 + 局部策略)与 CRTP 编译期骨架;好莱坞原则「别调用我们,我们会调用你」是它的精神注脚。文末对照 std::pmr::memory_resource 的 NVI、POCO Application 生命周期与 AOSP Activity。

【关键词】:模板方法、算法骨架、钩子、NVI、控制反转、好莱坞原则

【代码基准】:C++17

1. 三个任务类,三份长得像又各缺一角的仪式

后台任务框架里已经有三个任务:报表任务、传感器任务、清理任务。它们都在做同一套「仪式」------初始化资源、循环干活、每轮捕获异常、最后收尾打印统计:

cpp 复制代码
// 说明性片段
// ❌ 仪式被复制了三份,各自缺一角
void ReportTask::run(int loops) {
  openFile();                  // 1. 初始化
  for (int i = 0; i < loops; ++i)
    writeRow(i);               // 2. 干活
  closeFile();                 // 3. 收尾
}                              // ← 忘了 try

void SensorTask::run(int loops) {
  if (!openPort())
    return;                    // 初始化失败检查
  for (int i = 0; i < loops; ++i) {
    try {
      readSample();
    } catch (...) {            // 捕获是抄来的
      errors_++;               // 统计口径还不同
    }
  }
  closePort();
  printStats();                // ← 收尾统计
}                              //    只有这份有

复制仪式的代价不止是啰嗦:三份代码已经开始漂移------异常捕获有的有有的无、失败处理各写各的、统计口径互不相同;下一个需求「所有任务统一加超时与日志」落下来,要改三处,将来是 N 处;而每份仪式里真正属于「这个任务」的差异,其实只有两三个函数。

结构诊断:「顺序与横切」是稳定的,「步骤实现」是可变的 ,两者被搅在同一个函数里。解法是把稳定的部分上提为基类里一个不可重写 的方法,把可变的步骤留成虚函数让子类填------这正是模板方法;它与第 22 篇中介者里「状态机化」、第 26 篇策略「换整个算法」构成第三种切法:不换算法、只留空

2. 模式意图与定义

  • 一句话定义 :定义一个操作中算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的前提下,重定义该算法的某些特定步骤。
    解决的问题:一族流程「顺序与横切逻辑」相同、「个别步骤」不同时,消除仪式复制并保住骨架不被子类改坏。
  • GoF 原文意图Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. (定义一个操作中算法的骨架。)GoF 在本节写下的原则比模式本身更有名------好莱坞原则(Hollywood Principle):别调用我们,我们会调用你。父类的骨架调用子类的填空,控制权在框架手里,反转的是传统的调用方向。
  • Refactoring Guru 的表述:模板方法在父类中定义算法的框架,允许子类在不修改结构的情况下重写算法的特定步骤;RG 的类比是策划活动------流程单(场地→流程→收尾)是骨架,具体环节交给不同团队承办。

三条定性:

  1. 骨架非虚(现代 C++ 里加 final。算法结构是模式保护的对象------顺序、异常边界、计费统计都不许子类重排;「能改的只有留给你的空」。
  2. 空分两档必填的纯虚原语 (不填不成器)与可选的默认钩子(默认空实现,按需覆盖)------让子类只实现真正关心的步骤,是模板方法用得体面的关键。
  3. C++ 的看家搭配是 NVI (Non-Virtual Interface,Herb Sutter 提出):公有非虚函数对外、私有/保护虚函数做填空------模板方法就是 NVI 思想的算法骨架版,第 5 节正面展开。

3. UML 图 + 结构说明

#mermaid-svg-gTR2bMXZ3SPWdbgD{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-gTR2bMXZ3SPWdbgD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gTR2bMXZ3SPWdbgD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gTR2bMXZ3SPWdbgD .error-icon{fill:#552222;}#mermaid-svg-gTR2bMXZ3SPWdbgD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gTR2bMXZ3SPWdbgD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gTR2bMXZ3SPWdbgD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gTR2bMXZ3SPWdbgD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gTR2bMXZ3SPWdbgD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gTR2bMXZ3SPWdbgD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gTR2bMXZ3SPWdbgD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gTR2bMXZ3SPWdbgD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gTR2bMXZ3SPWdbgD .marker.cross{stroke:#333333;}#mermaid-svg-gTR2bMXZ3SPWdbgD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gTR2bMXZ3SPWdbgD p{margin:0;}#mermaid-svg-gTR2bMXZ3SPWdbgD g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-gTR2bMXZ3SPWdbgD g.classGroup text .title{font-weight:bolder;}#mermaid-svg-gTR2bMXZ3SPWdbgD .cluster-label text{fill:#333;}#mermaid-svg-gTR2bMXZ3SPWdbgD .cluster-label span{color:#333;}#mermaid-svg-gTR2bMXZ3SPWdbgD .cluster-label span p{background-color:transparent;}#mermaid-svg-gTR2bMXZ3SPWdbgD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gTR2bMXZ3SPWdbgD .cluster text{fill:#333;}#mermaid-svg-gTR2bMXZ3SPWdbgD .cluster span{color:#333;}#mermaid-svg-gTR2bMXZ3SPWdbgD .nodeLabel,#mermaid-svg-gTR2bMXZ3SPWdbgD .edgeLabel{color:#131300;}#mermaid-svg-gTR2bMXZ3SPWdbgD .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-gTR2bMXZ3SPWdbgD .label text{fill:#131300;}#mermaid-svg-gTR2bMXZ3SPWdbgD .labelBkg{background:#ECECFF;}#mermaid-svg-gTR2bMXZ3SPWdbgD .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-gTR2bMXZ3SPWdbgD .classTitle{font-weight:bolder;}#mermaid-svg-gTR2bMXZ3SPWdbgD .node rect,#mermaid-svg-gTR2bMXZ3SPWdbgD .node circle,#mermaid-svg-gTR2bMXZ3SPWdbgD .node ellipse,#mermaid-svg-gTR2bMXZ3SPWdbgD .node polygon,#mermaid-svg-gTR2bMXZ3SPWdbgD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gTR2bMXZ3SPWdbgD .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-gTR2bMXZ3SPWdbgD g.clickable{cursor:pointer;}#mermaid-svg-gTR2bMXZ3SPWdbgD g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-gTR2bMXZ3SPWdbgD g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-gTR2bMXZ3SPWdbgD .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-gTR2bMXZ3SPWdbgD .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-gTR2bMXZ3SPWdbgD .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-gTR2bMXZ3SPWdbgD .dashed-line{stroke-dasharray:3;}#mermaid-svg-gTR2bMXZ3SPWdbgD .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-gTR2bMXZ3SPWdbgD #compositionStart,#mermaid-svg-gTR2bMXZ3SPWdbgD .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-gTR2bMXZ3SPWdbgD #compositionEnd,#mermaid-svg-gTR2bMXZ3SPWdbgD .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-gTR2bMXZ3SPWdbgD #dependencyStart,#mermaid-svg-gTR2bMXZ3SPWdbgD .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-gTR2bMXZ3SPWdbgD #dependencyStart,#mermaid-svg-gTR2bMXZ3SPWdbgD .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-gTR2bMXZ3SPWdbgD #extensionStart,#mermaid-svg-gTR2bMXZ3SPWdbgD .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-gTR2bMXZ3SPWdbgD #extensionEnd,#mermaid-svg-gTR2bMXZ3SPWdbgD .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-gTR2bMXZ3SPWdbgD #aggregationStart,#mermaid-svg-gTR2bMXZ3SPWdbgD .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-gTR2bMXZ3SPWdbgD #aggregationEnd,#mermaid-svg-gTR2bMXZ3SPWdbgD .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-gTR2bMXZ3SPWdbgD #lollipopStart,#mermaid-svg-gTR2bMXZ3SPWdbgD .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-gTR2bMXZ3SPWdbgD #lollipopEnd,#mermaid-svg-gTR2bMXZ3SPWdbgD .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-gTR2bMXZ3SPWdbgD .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-gTR2bMXZ3SPWdbgD .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gTR2bMXZ3SPWdbgD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gTR2bMXZ3SPWdbgD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gTR2bMXZ3SPWdbgD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 只填空,不改骨架
<<abstract>>
Task
+run(loops) : int
#name() : string
#onInit() : bool
#onTick(i) : void
#onStop() : void
ReportTask
#onInit() : bool
#onTick(i) : void
#onStop() : void
SensorTask
#onInit() : bool
#onTick(i) : void
run() 非虚:\ninit → 循环(try/onTick)\n→ onStop → 统计

两个参与者:

  • 抽象类(Abstract Class)Task------run() 是模板方法(非虚,骨架 + 横切逻辑),onInit/onTick 是必填原语,onStop 是可选钩子;
  • 具体类(Concrete Class):填空。填的内容千差万别,流程结构一个模子。

值得盯着图想清楚的一件事:依赖箭头反转了 。平时是子类(更具体、后写的)调用基类;这里是基类(骨架、先写的)调用子类------「框架调用你」正是好莱坞原则的图上形态,也是控制反转(IoC)最古老的形态。第 6 篇工厂方法讲过「框架知道 when、不知道 what」,那句话在这里同样成立,只是产品换成了「步骤」。

4. 传统 C++ 写法(C++11 之前)

C++98 完整形态:骨架公有非虚、原语与钩子放 protected、子类只填空:

cpp 复制代码
// C++98/03 写法
#include <exception>
#include <stdio.h>

class Task {
public:
  virtual ~Task() {}

  // ---- 模板方法:骨架,非虚 ----
  int run(int loops) {
    if (!onInit()) {
      printf("[%s] 初始化失败\n",
             name());
      return 1;
    }

    int failures = 0;
    for (int i = 0; i < loops; ++i) {
      try {
        onTick(i);          // 子类填空
      } catch (
          const std::exception&
              e) {           // 横切:异常边界
        ++failures;
        printf(
            "[%s] 第 %d 轮失败:%s\n",
            name(), i, e.what());
      }
    }

    onStop();               // 可选钩子

    printf("[%s] 完成,失败 %d 次\n",
           name(), failures);
    return 0;
  }

protected:
  Task() {}

  virtual const char* name()
      const = 0;            // 必填
  virtual bool onInit() = 0;   // 必填
  virtual void onTick(int i) = 0;  // 必填
  virtual void onStop() {}   // 可选:默认空

private:
  Task(const Task&);
  Task& operator=(const Task&);
};

// ---- 具体任务一:报表,只填自己的空 ----
class ReportTask : public Task {
public:
  ReportTask() : rows_(0) {}

protected:
  const char* name() const {
    return "report";
  }

  bool onInit() {
    printf("  打开报表文件\n");
    return true;
  }

  void onTick(int i) {
    printf("  写第 %d 行\n", i);
    ++rows_;
  }

  void onStop() {           // 需要收尾,覆盖
    printf("  共 %d 行,落盘\n", rows_);
  }

private:
  int rows_;
};

// ---- 具体任务二:传感器 ----
class SensorTask : public Task {
protected:
  const char* name() const {
    return "sensor";
  }

  bool onInit() {
    printf("  打开串口\n");
    return true;
  }

  void onTick(int i) {
    printf("  读样本 %d\n", i);
  }
  // onStop 不需要,用默认空实现
};

int main() {
  ReportTask report;
  SensorTask sensor;
  report.run(2);
  sensor.run(3);
  return 0;
}

对照第 1 节:三份仪式收拢成一份骨架,「统一加超时」只改 run() 一处;漂移不可能再发生------异常边界、失败统计只有一份实现;每个子类只剩自己的血肉。

三条传统写法的铁律:骨架非虚且不可被子类遮蔽 ------C++98 没有 final,只能靠命名与评审守护(现代版补上),谁在子类里重写了 run(),谁就掏空了模式;钩子分档 ------必填的纯虚逼子类表态,可选的默认空钩子不逼任何人,两档用反了不是「填不完的空」就是「静默走错路」;填空方法放 protected 而非 public ------外界只该看见 run(),直接调 onTick 会绕过骨架的横切逻辑,这就是 NVI 的雏形。

5. 现代 C++ 进阶写法

升级零:final + override,骨架终于有了牙齿

cpp 复制代码
// 节选:骨架上锁
class Task {
public:
  virtual ~Task() = default;

  int run(int loops) /* final */;

protected:
  virtual bool onInit() = 0;
  virtual void onTick(int i) = 0;
  virtual void onStop() {}
};

// 若 run 本身可能被再派生层扩展,
// 用一个非虚入口 + 骨架分离的结构:
class Task2 {
public:
  virtual ~Task2() = default;

  int run(int loops) final {   // 对外永远这个门
    return runSkeleton(loops);
  }

private:
  virtual int runSkeleton(int loops) = 0;
  // NVI 完全体:公有非虚 final,
  // 私有虚函数做实现
};

NVI 惯用法 值得单独记一笔(Herb Sutter《Virtuality》):公有接口一律非虚,虚函数一律私有或保护。好处有三:调用前置/后置条件(校验、加锁、计数)写进非虚外壳,子类无法绕过;虚接口不进公共 ABI,演化自由;调用方不可能误用「半个流程」。模板方法与 NVI 是同一思想的两级应用------前者固化整条算法,后者固化单个入口的边界。

改进一:函数注入版------骨架保留、继承消失。把「填空」从继承改为注入可调用对象,与第 26 篇策略合流成「骨架 + 局部策略」:

cpp 复制代码
// 节选:无继承的任务骨架
#include <functional>

class TaskRunner {
public:
  using Init = std::function<bool()>;
  using Tick =
      std::function<void(int)>;
  using Stop = std::function<void()>;

  TaskRunner(std::string name,
             Init init, Tick tick,
             Stop stop = nullptr)
      : name_(std::move(name)),
        init_(std::move(init)),
        tick_(std::move(tick)),
        stop_(std::move(stop)) {}

  int run(int loops) {
    if (!init_())
      return 1;
    for (int i = 0; i < loops; ++i)
      tick_(i);
    if (stop_)
      stop_();
    return 0;
  }

private:
  std::string name_;
  Init init_;
  Tick tick_;
  Stop stop_;
};

// 装配一个任务,不再有子类:
// TaskRunner t("sensor",
//     [] { return openPort(); },
//     [](int i) { readSample(); },
//     [] { closePort(); });

继承版与注入版的取舍是第 26 篇「委托 vs 继承」的延续:填空者要访问大量受保护状态、且族内演化 用继承版;填空只是几个函数、想运行时组合用注入版。两版骨架共享,横切逻辑仍只此一份。

改进二:CRTP 静态骨架------编译期模板方法。填空在编译期已知时,模板基类把虚调用换成静态分发:

cpp 复制代码
// 节选:编译期骨架
#include <cstdio>

template <typename Derived>
class TaskBase {
public:
  int run(int loops) {
    for (int i = 0; i < loops; ++i)
      self().onTick(i);   // 静态分派
    return 0;
  }

private:
  Derived& self() {
    return static_cast<Derived&>(*this);
  }
};

class ReportTask
    : public TaskBase<ReportTask> {
public:
  void onTick(int i) {
    std::printf("写第 %d 行\n", i);
  }
};

与第 13 篇 CRTP mixin、第 16 篇编译期桥同一谱系:骨架照旧固化,虚表与间接全部消失。展望 :C++23 显式对象形参(deducing this)让 CRTP 的 self() 样板消失,静态骨架的写法会再瘦一圈。

6. 优缺点与适用场景

  • ✅ 优点(GoF 后果清单):骨架与横切只写一次 ------顺序、异常边界、统计、日志集中基类,漂移根除;子类只填空 ------新增一族流程只写差异,仪式零复制;控制反转------框架掌握流程,扩展点清晰可枚举(钩子列表就是扩展点清单)。
  • ❌ 缺点:继承是硬耦合 ------子类与基类的骨架、生命周期、受保护状态绑死,跨族复用要靠复制(注入版补此短板);骨架演化伤全员 ------加一步钩子、改一次顺序,全部子类重新审视;骨架难单测------测试子类就会把骨架拖进来,常需「假子类」配合。
  • 🎯 适用场景:框架生命周期(onInit/onStart/onStop)、解析与导出管线(连接→读→转换→写→关闭)、测试夹具(setUp/tearDown)、一族流程只在个别步骤上差异的任何地方。

〔辨析〕模板方法 vs 策略(第 26 篇,正面清算):继承填空 vs 组合换算法 ------模板方法换的是骨架里的局部步骤(结构锁死),策略换的是整条算法(结构不管);模板方法编译期定(注入版除外)、子类是「族成员」,策略运行期可换、实现是「外来件」。GoF 原书的说法:两者像一道菜的两种做法------磨碎(细粒度钩子)还是整块(整体替换)。模板方法 vs 工厂方法(第 6 篇):工厂方法是「创建步骤的模板方法特例」------第 6 篇的 CreateMaze() 骨架调 MakeRoom() 填空,正是本篇;模板方法 vs 外观(第 14 篇):外观收拢调用方 的仪式(客户端少写),模板方法收拢被调方的骨架(框架掌舵)------一个向下简化,一个向上控制。

7. 开源项目中的身影

标准库:std::pmr::memory_resource 是教科书级 NVI 。公共接口 allocate 是非虚函数(做大小对齐校验、转发),真正的定制点 do_allocate私有纯虚------Sutter 的 NVI 建议被标准库原样执行:

cpp 复制代码
// 标准库骨架(节选,语义保留)
class memory_resource {
public:
  void* allocate(
      size_t bytes,
      size_t alignment) {
    // 非虚外壳:校验与公共逻辑
    return do_allocate(bytes,
                       alignment);
  }

private:
  virtual void* do_allocate(
      size_t, size_t) = 0;  // 定制点
};
// 用户派生 pool_resource 等实现,
// 外界永远只见 allocate()

点评:第 16 篇曾把 std::pmr 当「分配器桥」读过,本篇再给它一个头衔------全标准库最规范的模板方法现场。连「定制点私有」这一最严格的 NVI 形态都被标准采纳,值得照抄。

POCO:Poco::Util::Application 的生命周期骨架 。写 POCO 应用就是填空:继承 Application,覆盖 initialize/main/uninitialize,框架负责加载配置、初始化日志、解析命令行(第 14 篇的 ParseCommandLine 呼应)、异常兜底,最后在恰当的时机调用你的 main

cpp 复制代码
// 说明性片段(需链接 PocoUtil)
class MyApp
    : public Poco::Util::Application {
protected:
  void initialize(
      Poco::Util::Application&
          self) override {
    // 配置与日志已由骨架就绪
  }

  int main(
      const std::vector<
          std::string>&) override {
    return 0;   // 你的业务只有这里
  }
};
// 入口只剩:
// POCO_APP_MAIN(MyApp)

点评:注意 main 这个填空的名字------框架连「程序入口」都收编了,你写的只是骨架中的一步。好莱坞原则的完整版:不是你运行程序,是程序运行你的代码。

AOSP:Activity 生命周期,好莱坞原则的全球最大现场 。Android 的 Activity 定义 onCreate → onStart → onResume → onPause → onStop → onDestroy 骨架,系统按生命周期事件驱动,应用只能填空、不能改序------数百万个「子类」在这个模板方法里运转,谁也别想自己调 onResume()

java 复制代码
// 说明性片段(Android 经典协议,
// 结构与 C++ 同构)
public class MainActivity
        extends Activity {
  @Override
  protected void onCreate(
      Bundle b) {   // 填空 1
    super.onCreate(b);
  }
  // onStart/onResume... 按需覆盖
}

点评:Activity 把本篇两条铁律演示到极致------骨架非虚(生命周期由系统调用)、钩子分档(onCreate 必答、onStop 可选)。它也暴露代价的极限形态:当骨架有一打钩子、族内有几十层继承时,「填了哪几个空」的追踪成本会压过骨架收益------官方随后推出「组合优先」的 Jetpack 组件,正是从模板方法向依赖注入的整体撤退。

三份代码合看:memory_resource 把 NVI 做进标准、POCO 把生命周期做进应用框架、Android 把它做给全世界------「别调用我们,我们会调用你」,每个框架的第一句话都是它。

本篇小结

一族流程「顺序与横切稳定、个别步骤可变」时,把骨架固化在基类的非虚方法里,子类只填空:仪式只写一次、漂移根除、扩展点可枚举。两条纪律保住模式的命:骨架非虚(final 上锁)、钩子分必填与可选两档;C++ 的看家搭配是 NVI------公有非虚做门、私有虚做定制点,std::pmr::memory_resource 是可照抄的范本。当继承的耦合开始发烫,注入版(骨架 + 函数)与策略模式在此接手。行为型十一种模式至此只剩最后一难------下一篇访问者模式:对稳定的结构加新的操作,双重分派,以及本系列埋了三篇的伏笔正式收线。

本文模式定义与角色划分参考了 Refactoring Guru《设计模式》中文版「模板方法」一章,意图译文与好莱坞原则参考了 GoF《Design Patterns》第 5 章 Template Method 一节,NVI 惯用法参考了 Herb Sutter 的《Virtuality》一文。

相关推荐
王老师青少年编程1 小时前
信奥赛csp初赛高频考点【排序算法】(详细解析)
c++·排序算法·csp-j·csp·高频考点·初赛·信奥赛
qeen871 小时前
【C++】C++中的错误处理机制-异常
开发语言·c++·异常
Zixhy1 小时前
Boost 库实现异步高并发服务器
linux·服务器·c++
AbandonForce2 小时前
简谈线程池
开发语言·c++·算法
Mr_liu_6662 小时前
ns3-gym例子解析_基础例子与wifi例子_DQN(3)
开发语言·c++·python·dqn·ns3
佳児素花痴╮2 小时前
C++速通2
开发语言·c++·算法
David猪大卫3 小时前
【C++修炼】异常
开发语言·c++·经验分享·笔记·学习·考研·面试
charlie1145141913 小时前
浅谈C++的列表初始化std::initializer_list
开发语言·数据结构·c++·list·开源项目
励志不掉头发的内向程序员3 小时前
【LibreCAD 2D架构】RS_Line创建之后发生了什么?从图形容器到屏幕渲染
开发语言·c++·qt·学习·系统架构