模板方法模式

用咖啡厅制作饮品的例子理解模板方法模式

问题

使用模板方法模式实现咖啡厅制作饮品的场景。咖啡和茶的制作流程相似,但具体步骤不同。

解答

模板方法模式概念

模板方法模式在父类中定义算法骨架,将部分步骤延迟到子类实现。父类控制流程,子类负责具体实现。

实现饮品制作

// 抽象类:饮品
class Beverage {
  // 模板方法,定义制作流程(不可被子类覆盖)
  make() {
    this.boilWater();
    this.brew();
    this.pourInCup();
    this.addCondiments();
  }

  // 通用步骤:烧水
  boilWater() {
    console.log('烧开水');
  }

  // 通用步骤:倒入杯中
  pourInCup() {
    console.log('倒入杯中');
  }

  // 抽象方法:冲泡(子类必须实现)
  brew() {
    throw new Error('子类必须实现 brew 方法');
  }

  // 抽象方法:添加调料(子类必须实现)
  addCondiments() {
    throw new Error('子类必须实现 addCondiments 方法');
  }
}

// 具体类:咖啡
class Coffee extends Beverage {
  brew() {
    console.log('用沸水冲泡咖啡粉');
  }

  addCondiments() {
    console.log('加糖和牛奶');
  }
}

// 具体类:茶
class Tea extends Beverage {
  brew() {
    console.log('用沸水浸泡茶叶');
  }

  addCondiments() {
    console.log('加柠檬');
  }
}

// 使用
const coffee = new Coffee();
console.log('--- 制作咖啡 ---');
coffee.make();

const tea = new Tea();
console.log('--- 制作茶 ---');
tea.make();

输出:

--- 制作咖啡 ---
烧开水
用沸水冲泡咖啡粉
倒入杯中
加糖和牛奶
--- 制作茶 ---
烧开水
用沸水浸泡茶叶
倒入杯中
加柠檬

添加钩子方法

钩子方法让子类可以选择性地改变模板流程:

class Beverage {
  make() {
    this.boilWater();
    this.brew();
    this.pourInCup();
    // 钩子:是否需要添加调料
    if (this.wantCondiments()) {
      this.addCondiments();
    }
  }

  boilWater() {
    console.log('烧开水');
  }

  pourInCup() {
    console.log('倒入杯中');
  }

  brew() {
    throw new Error('子类必须实现 brew 方法');
  }

  addCondiments() {
    throw new Error('子类必须实现 addCondiments 方法');
  }

  // 钩子方法:默认需要调料,子类可覆盖
  wantCondiments() {
    return true;
  }
}

// 黑咖啡:不加调料
class BlackCoffee extends Beverage {
  brew() {
    console.log('用沸水冲泡咖啡粉');
  }

  addCondiments() {
    console.log('加糖和牛奶');
  }

  // 覆盖钩子:不要调料
  wantCondiments() {
    return false;
  }
}

const blackCoffee = new BlackCoffee();
console.log('--- 制作黑咖啡 ---');
blackCoffee.make();

输出:

--- 制作黑咖啡 ---
烧开水
用沸水冲泡咖啡粉
倒入杯中

函数式实现

不用类,用高阶函数实现:

// 模板函数
function makeBeverage(options) {
  const { brew, addCondiments, wantCondiments = () => true } = options;

  return function () {
    console.log('烧开水');
    brew();
    console.log('倒入杯中');
    if (wantCondiments()) {
      addCondiments();
    }
  };
}

// 创建具体饮品
const makeCoffee = makeBeverage({
  brew: () => console.log('用沸水冲泡咖啡粉'),
  addCondiments: () => console.log('加糖和牛奶'),
});

const makeTea = makeBeverage({
  brew: () => console.log('用沸水浸泡茶叶'),
  addCondiments: () => console.log('加柠檬'),
});

console.log('--- 制作咖啡 ---');
makeCoffee();

console.log('--- 制作茶 ---');
makeTea();

关键点

  • 模板方法:父类定义算法骨架,固定执行顺序
  • 抽象方法:子类必须实现的步骤,体现差异化
  • 钩子方法:子类可选覆盖,用于控制流程分支
  • 好莱坞原则:「别调用我们,我们会调用你」——父类控制流程,子类被动调用
  • 适用场景:多个类有相同算法结构,但部分步骤实现不同