设计模式之模板方法模式

设计模式之模板方法模式

意图

模板方法模式 (Template Method) 是一种行为设计模式, 它在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。

模板方法模式是所有模式中最为常见的几个模式之一,是基于继承代码复用的基本技术。,没有关联关系。 因此,在模板方法模式的类结构图中,只有继承关系

模板方法模式需要开发抽象类和具体子类的设计师之间的协作。一个设计师负责给出一个算法的轮廓和骨架,另一些设计师则负责给出这个算法的各个逻辑步骤。

代表这些具体逻辑步骤的方法称做**基本方法(primitive method);而将这些基本方法汇总起来的方法叫做模板方法(template method)**,这个设计模式的名字就是从此而来。

在模板方法模式中,首先父类会定义一个算法的框架,即实现算法所必须的所有方法。

  • 其中,具有共性的代码放在父类的具体方法中。

  • 各个子类特殊性的代码放在子类的具体方法中。但是父类中需要有对应抽象方法声明。

  • 钩子方法可以让子类决定是否对算法的不同点进行挂钩。

适用场景

  • 当你只希望客户端扩展某个特定算法步骤, 而不是整个算法或其结构时, 可使用模板方法模式。
  • 当多个类的算法除一些细微不同之外几乎完全一样时, 你可使用该模式。 但其后果就是, 只要算法发生变化, 你就可能需要修改所有的类。

结构

img

结构说明

  1. 抽象类 (Abstract­Class) 会声明作为算法步骤的方法, 以及依次调用它们的实际模板方法。 算法步骤可以被声明为 抽象类型, 也可以提供一些默认实现。
  2. 具体类 (Concrete­Class) 可以重写所有步骤, 但不能重写模板方法自身。

结构代码范式

AbstractClass : 抽象类,定义并实现一个模板方法。这个模板方法定义了算法的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类去实现。顶级逻辑也有可能调用一些具体方法。

1
2
3
4
5
6
7
8
9
abstract class AbstractClass {
public abstract void PrimitiveOperation1();
public abstract void PrimitiveOperation2();

public void TemplateMethod() {
PrimitiveOperation1();
PrimitiveOperation2();
}
}

ConcreteClass : 实现实现父类所定义的一个或多个抽象方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ConcreteClassA extends AbstractClass {
@Override
public void PrimitiveOperation1() {
System.out.println("具体A类方法1");
}

@Override
public void PrimitiveOperation2() {
System.out.println("具体A类方法2");
}
}

class ConcreteClassB extends AbstractClass {
@Override
public void PrimitiveOperation1() {
System.out.println("具体B类方法1");
}

@Override
public void PrimitiveOperation2() {
System.out.println("具体B类方法2");
}
}

客户端

1
2
3
4
5
6
7
8
public class TemplateMethodPattern {
public static void main(String[] args) {
AbstractClass objA = new ConcreteClassA();
AbstractClass objB = new ConcreteClassB();
objA.TemplateMethod();
objB.TemplateMethod();
}
}

伪代码

本例中的模板方法模式为一款简单策略游戏中人工智能的不同分支提供 “框架”。

模板方法模式示例的结构

一款简单游戏的 AI 类。

游戏中所有的种族都有几乎同类的单位和建筑。 因此你可以在不同的种族上复用相同的 AI 结构, 同时还需要具备重写一些细节的能力。 通过这种方式, 你可以重写半兽人的 AI 使其更富攻击性, 也可以让人类侧重防守, 还可以禁止怪物建造建筑。 在游戏中新增种族需要创建新的 AI 子类, 还需要重写 AI 基类中所声明的默认方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 抽象类定义了一个模板方法,其中通常会包含某个由抽象原语操作调用组成的算
// 法框架。具体子类会实现这些操作,但是不会对模板方法做出修改。
class GameAI is
// 模板方法定义了某个算法的框架。
method turn() is
collectResources()
buildStructures()
buildUnits()
attack()

// 某些步骤可在基类中直接实现。
method collectResources() is
foreach (s in this.builtStructures) do
s.collect()

// 某些可定义为抽象类型。
abstract method buildStructures()
abstract method buildUnits()

// 一个类可包含多个模板方法。
method attack() is
enemy = closestEnemy()
if (enemy == null)
sendScouts(map.center)
else
sendWarriors(enemy.position)

abstract method sendScouts(position)
abstract method sendWarriors(position)

// 具体类必须实现基类中的所有抽象操作,但是它们不能重写模板方法自身。
class OrcsAI extends GameAI is
method buildStructures() is
if (there are some resources) then
// 建造农场,接着是谷仓,然后是要塞。

method buildUnits() is
if (there are plenty of resources) then
if (there are no scouts)
// 建造苦工,将其加入侦查编组。
else
// 建造兽族步兵,将其加入战士编组。

// ...

method sendScouts(position) is
if (scouts.length > 0) then
// 将侦查编组送到指定位置。

method sendWarriors(position) is
if (warriors.length > 5) then
// 将战斗编组送到指定位置。

// 子类可以重写部分默认的操作。
class MonstersAI extends GameAI is
method collectResources() is
// 怪物不会采集资源。

method buildStructures() is
// 怪物不会建造建筑。

method buildUnits() is
// 怪物不会建造单位。

案例

模板方法模式应用场景十分广泛。

在《Head First》的模板方法模式章节里列举了一个十分具有代表性的例子。

现实生活中,茶和咖啡是随处可见的饮料。冲泡一杯茶或冲泡一杯咖啡的过程是怎样的?

我们来整理一下流程。

  • 泡茶: 烧开水 ==> 冲泡茶叶 ==> 倒入杯中 ==> 添加柠檬
  • 泡咖啡: 烧开水 ==> 冲泡咖啡 ==> 倒入杯中 ==> 添加糖和牛奶

由以上处理步骤不难发现,准备这两种饮料的处理过程非常相似。我们可以使用模板类方法去限定制作饮料的算法框架。

其中相同的具有共性的步骤(如烧开水、倒入杯中),直接在抽象类中给出具体实现。

而对于有差异性的步骤,则在各自的具体类中给出实现。

【抽象类】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
abstract class Beverage {

// 模板方法,决定了算法骨架。相当于TemplateMethod()方法
public void prepareBeverage() {
boilWater();
brew();
pourInCup();
if (customWantsCondiments())
{
addCondiments();
}
}

// 共性操作,直接在抽象类中定义
public void boilWater() {
System.out.println("烧开水");
}

// 共性操作,直接在抽象类中定义
public void pourInCup() {
System.out.println("倒入杯中");
}

// 钩子方法,决定某些算法步骤是否挂钩在算法中
public boolean customWantsCondiments() {
return true;
}

// 特殊操作,在子类中具体实现
public abstract void brew();

// 特殊操作,在子类中具体实现
public abstract void addCondiments();

}

【具体类】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Tea extends Beverage {

@Override
public void brew() {
System.out.println("冲泡茶叶");
}

@Override
public void addCondiments() {
System.out.println("添加柠檬");
}

}

class Coffee extends Beverage {

@Override
public void brew() {
System.out.println("冲泡咖啡豆");
}

@Override
public void addCondiments() {
System.out.println("添加糖和牛奶");
}

}

【客户端】

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {

System.out.println("============= 准备茶 =============");
Beverage tea = new Tea();
tea.prepareBeverage();

System.out.println("============= 准备咖啡 =============");
Beverage coffee = new Coffee();
coffee.prepareBeverage();

}

输出

1
2
3
4
5
6
7
8
9
10
============= 准备茶 =============
烧开水
冲泡茶叶
倒入杯中
添加柠檬
============= 准备咖啡 =============
烧开水
冲泡咖啡豆
倒入杯中
添加糖和牛奶

案例

使用示例: 模版方法模式在 Java 框架中很常见。 开发者通常使用它来向框架用户提供通过继承实现的、 对标准功能进行扩展的简单方式。

这里是一些核心 Java 程序库中模版方法的示例:

识别方法: 模版方法可以通过行为方法来识别, 该方法已有一个在基类中定义的 “默认” 行为。

消除 if … else 和重复代码

假设要开发一个购物车功能,针对不同用户进行不同的处理:

  • 普通用户需要收取运费,运费是商品价格的 10%,无商品折扣;
  • VIP 用户同样需要收取商品价格 10% 的快递费,但购买两件以上相同商品时,第三件开始享受一定折扣;
  • 内部用户可以免运费,无商品折扣。

问题 1.0 版本

普通用户购物车

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class NormalUserCart {

public Cart process(long userId, Map<Long, Integer> items) {
Cart cart = new Cart();

//把Map的购物车转换为Item列表
List<Item> itemList = new ArrayList<>();
items.entrySet().stream().forEach(entry -> {
Item item = new Item();
item.setId(entry.getKey());
item.setPrice(Db.getItemPrice(entry.getKey()));
item.setQuantity(entry.getValue());
itemList.add(item);
});
cart.setItems(itemList);

//处理运费和商品优惠
itemList.stream().forEach(item -> {
//运费为商品总价的10%
item.setDeliveryPrice(
item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal("0.1")));
//无优惠
item.setCouponPrice(BigDecimal.ZERO);
});

//计算纯商品总价
cart.setTotalItemPrice(cart.getItems()
.stream()
.map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add));
//计算运费总价
cart.setTotalDeliveryPrice(
cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
//计算总优惠
cart.setTotalDiscount(
cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
//应付总价=商品总价+运费总价-总优惠
cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
return cart;
}

}

VIP 用户购物车

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class VipUserCart {

public Cart process(long userId, Map<Long, Integer> items) {
Cart cart = new Cart();

List<Item> itemList = new ArrayList<>();
items.entrySet().stream().forEach(entry -> {
Item item = new Item();
item.setId(entry.getKey());
item.setPrice(Db.getItemPrice(entry.getKey()));
item.setQuantity(entry.getValue());
itemList.add(item);
});
cart.setItems(itemList);

itemList.stream().forEach(item -> {
//运费为商品总价的10%
item.setDeliveryPrice(
item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal("0.1")));
//购买两件以上相同商品,第三件开始享受一定折扣
if (item.getQuantity() > 2) {
item.setCouponPrice(item.getPrice()
.multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId)).divide(new BigDecimal("100")))
.multiply(BigDecimal.valueOf(item.getQuantity() - 2)));
} else {
item.setCouponPrice(BigDecimal.ZERO);
}
});

cart.setTotalItemPrice(cart.getItems()
.stream()
.map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setTotalDeliveryPrice(
cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setTotalDiscount(
cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
return cart;
}

}

内部用户购物车

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class InternalUserCart {

public Cart process(long userId, Map<Long, Integer> items) {
Cart cart = new Cart();

List<Item> itemList = new ArrayList<>();
items.entrySet().stream().forEach(entry -> {
Item item = new Item();
item.setId(entry.getKey());
item.setPrice(Db.getItemPrice(entry.getKey()));
item.setQuantity(entry.getValue());
itemList.add(item);
});
cart.setItems(itemList);

itemList.stream().forEach(item -> {
//免运费
item.setDeliveryPrice(BigDecimal.ZERO);
//无优惠
item.setCouponPrice(BigDecimal.ZERO);
});

cart.setTotalItemPrice(cart.getItems()
.stream()
.map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setTotalDeliveryPrice(
cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setTotalDiscount(
cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
return cart;
}

}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@GetMapping("wrong")
public Cart wrong(@RequestParam("userId") int userId) {
String userCategory = Db.getUserCategory(userId);

if (userCategory.equals("Normal")) {
NormalUserCart normalUserCart = new NormalUserCart();
return normalUserCart.process(userId, items);
}

if (userCategory.equals("Vip")) {
VipUserCart vipUserCart = new VipUserCart();
return vipUserCart.process(userId, items);
}

if (userCategory.equals("Internal")) {
InternalUserCart internalUserCart = new InternalUserCart();
return internalUserCart.process(userId, items);
}

return null;
}

对比一下代码量可以发现,三种购物车 70% 的代码是重复的。原因很简单,虽然不同类型用户计算运费和优惠的方式不同,但整个购物车的初始化、统计总价、总运费、总优惠和支付价格的逻辑都是一样的。

修正版本

1.0 版本的问题在于:相同的代码应该只在一处出现。

如果我们熟记抽象类和抽象方法的定义的话,这时或许就会想到,是否可以把重复的逻辑定义在抽象类中,三个购物车只要分别实现不同的那份逻辑呢?

其实,这个模式就是模板方法模式。

下面展示基于 工厂模式+模板方法模式 优化重复代码。

【抽象类】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public abstract class AbstractCart {

public Cart process(long userId, Map<Long, Integer> items) {

Cart cart = new Cart();

List<Item> itemList = new ArrayList<>();
items.entrySet().stream().forEach(entry -> {
Item item = new Item();
item.setId(entry.getKey());
item.setPrice(Db.getItemPrice(entry.getKey()));
item.setQuantity(entry.getValue());
itemList.add(item);
});
cart.setItems(itemList);

itemList.stream().forEach(item -> {
processCouponPrice(userId, item);
processDeliveryPrice(userId, item);
});

cart.setTotalItemPrice(cart.getItems()
.stream()
.map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setTotalDeliveryPrice(
cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setTotalDiscount(
cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));
return cart;
}

protected abstract void processCouponPrice(long userId, Item item);

protected abstract void processDeliveryPrice(long userId, Item item);

}

【普通用户购物车】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service(value = "NormalUserCart")
public class NormalUserCart extends AbstractCart {

@Override
protected void processCouponPrice(long userId, Item item) {
item.setCouponPrice(BigDecimal.ZERO);
}

@Override
protected void processDeliveryPrice(long userId, Item item) {
item.setDeliveryPrice(item.getPrice()
.multiply(BigDecimal.valueOf(item.getQuantity()))
.multiply(new BigDecimal("0.1")));
}

}

【VIP 用户购物车】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service(value = "VipUserCart")
public class VipUserCart extends NormalUserCart {

@Override
protected void processCouponPrice(long userId, Item item) {
if (item.getQuantity() > 2) {
item.setCouponPrice(item.getPrice()
.multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId)).divide(new BigDecimal("100")))
.multiply(BigDecimal.valueOf(item.getQuantity() - 2)));
} else {
item.setCouponPrice(BigDecimal.ZERO);
}
}

}

【内部用户购物车】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service(value = "InternalUserCart")
public class InternalUserCart extends AbstractCart {

@Override
protected void processCouponPrice(long userId, Item item) {
item.setCouponPrice(BigDecimal.ZERO);
}

@Override
protected void processDeliveryPrice(long userId, Item item) {
item.setDeliveryPrice(BigDecimal.ZERO);
}

}

【客户端】

1
2
3
4
5
6
@GetMapping("right")
public Cart right(@RequestParam("userId") int userId) {
String userCategory = Db.getUserCategory(userId);
AbstractCart cart = (AbstractCart) applicationContext.getBean(userCategory + "UserCart");
return cart.process(userId, items);
}

与其他模式的关系

  • 工厂方法模式模板方法模式的一种特殊形式。 同时, 工厂方法可以作为一个大型模板方法中的一个步骤。
  • 模板方法基于继承机制: 它允许你通过扩展子类中的部分内容来改变部分算法。 策略模式基于组合机制: 你可以通过对相应行为提供不同的策略来改变对象的部分行为。 模板方法在类层次上运作, 因此它是静态的。 策略在对象层次上运作, 因此允许在运行时切换行为。

参考资料