在超类/接口中定义一个算法的骨架,而将一些具体步骤的实现延迟到子类中。

模板方法使得子类可以不改变算法的步骤即可重定义该算法某些步骤的具体实现。

背景

假如正在开发一款分析文档的数据挖掘程序。

用户需要向程序输入各种格式(PDF、DOC或CSV)的文档,程序则会试图从这些文件中抽取有意义的数据,并以统一的格式将其返回给用户。

这三个类中包含许多相似代码。尽管这些类处理不同数据格式的代码完全不同,但数据处理和分析的代码却几乎完全一样。

如何能去除重复代码?

还有另一个与使用这些类的客户端代码相关的问题:客户端代码中包含许多条件语句,以根据不同的处理对象类型选择合适的处理过程。

如果所有处理数据的类都拥有相同的接口或基类, 那么就可以去除客户端代码中的条件语句,转而使用多态机制来在处理对象上调用函数。

解决方案

模板方法模式建议将算法分解为一系列步骤,然后将这些步骤改写为方法,最后在一个 “模板方法” 中依次调用这些方法。

  • 步骤可以是抽象的,也可以有一些默认的实现。
  • 为了能够使用算法,客户端需要自行提供子类并实现所有的抽象步骤。
  • 如有必要还需重写一些步骤,但这不包括模板方法自身。

有两种类型的步骤:

  • 抽象步骤必须由各个子类来实现
  • 可选步骤已有一些默认实现,但仍可在需要时进行重写(在golang中的组合实现不如java继承方便)
    • 还有另一种名为钩子的步骤。 钩子是内容为空的可选步骤。
    • 即使不重写钩子,模板方法也能工作。
    • 钩子通常放置在算法重要步骤的前后,为子类提供额外的算法扩展点。

模板模式定义算法骨架,使用上有两个特征

  • 一是要继承算法骨架,达到复用的目的
  • 二是具体的算法步骤在子类中实现,达到扩展的目的。

模板方法

模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。

以前用这种定义好算法骨架,具体实现在不同子类的方案时,一般使用的是工厂方法加代理模式。

工厂方法能够提供更多的灵活性,但如果一个算法骨架中有10个具体算法,总不能让工厂生产10个不同的对象吧。 所以如果算法骨架中有多个具体算法,而这些算法又是高内聚的,用模板模式就很合适。

适用场景

  • 当你只希望客户端扩展某个特定算法步骤,而不是整个算法或其结构时,可使用模板方法模式。
    • 模板方法将整个算法转换为一系列独立的步骤,以便子类能对其进行扩展,同时还可让超类中所定义的结构保持完整。
  • 当多个类的算法除一些细微不同之外几乎完全一样时,可使用该模式。
    • 在将算法转换为模板方法时,你可将相似的实现步骤提取到超类中以去除重复代码。子类间各不相同的代码可继续保留在子类中。

实现步骤

  1. 分析目标算法,确定能否将其分解为多个步骤。从所有子类的角度出发,考虑哪些步骤能够通用,哪些步骤各不相同。
  2. 创建抽象基类并声明一个模板方法和代表算法步骤的一系列抽象方法。在模板方法中根据算法结构依次调用相应步骤。可用final最终修饰模板方法以防止子类对其进行重写。
  3. 虽然可将所有步骤全都设为抽象类型,但默认实现可能会给部分步骤带来好处,因为子类无需实现那些方法。
  4. 可考虑在算法的关键步骤前后添加钩子,为子类提供扩展点。
  5. 为每个算法变体新建一个具体子类,它必须实现所有的抽象步骤,也可以重写部分可选步骤。

优缺点

优点:

  • 可仅允许客户端重写一个大型算法中的特定部分,使得算法其他部分修改对其所造成的影响减小。
  • 可将重复代码提取到一个超类中。

缺点:

  • 部分客户端可能会受到算法框架的限制。
  • 通过子类替代默认步骤实现可能会导致违反里氏替换原则(子类步骤和父类步骤实现不同)。
  • 模板方法中的步骤越多,其维护工作就可能会越困难。

与其它模式的关系

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

总结

模板模式有两大作用:复用和扩展。

  • 复用指的是,所有的子类可以复用父类中提供的模板方法的代码。
  • 扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。

样例

实现一

 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
package main

// IWork 上班接口
type IWork interface {
	Getup()   // 起床
	Commute() // 通勤
	Arrive()  // 到达
}

// BaseWork 提供给客户端使用的基类
// Golang是面向组合的思想,没有抽象类的概念
// 需要将接口(子类实现)作为属性组合进来,或者作为GotoWork()的参数传递进来
type BaseWork struct {
	Iw IWork // 包含一个具体子类的引用
}

// GotoWork 操作方法,调用具体子类的实现
func (bw *BaseWork) GotoWork() error {
	bw.Iw.Getup()
	bw.Iw.Commute()
	bw.Iw.Arrive()
	return nil
}

// Getup 起床,父类的默认实现
func (bw *BaseWork) Getup() {
	print("getup.")
}

// Commute 通勤,父类的默认实现
func (bw *BaseWork) Commute() {
	print("commute.")
}

// Arrive 到达,父类的默认实现
func (bw *BaseWork) Arrive() {
	print("arrive.")
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

// Drive 具体实现
type Drive struct {
	BaseWork
}

// Commute 重写父类的默认方法
func (d Drive) Commute() {
	print("commute by driving car.")
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

// Bus 具体实现
type Bus struct {
	BaseWork
}

// Commute 通勤,子类重写
func (b Bus) Commute() {
	print("commute by bus.")
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

func main() {
	var base = BaseWork{}

	base.Iw = &Bus{}
	_ = base.GotoWork()

	base.Iw = &Drive{}
	_ = base.GotoWork()

	// bus.GotoWork()
	// 其实也可以使用子类对象调用,但是理解起来比较绕
}

实现二

 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
package main

// IWork 上班接口
type IWork interface {
	Getup()                    // 起床
	Commute()                  // 通勤
	Arrive()                   // 到达
	GotoWork(work IWork) error // 实现上班功能
}

// BaseWork 提供给客户端使用的基类
// Golang是面向组合的思想,没有抽象类的概念
// 需要将接口(子类对象)作为属性组合进来,或者作为GotoWork()的参数传递进来
type BaseWork struct {
}

// GotoWork 操作方法,调用具体子类的实现,将子类对象作为参数传递
func (bw *BaseWork) GotoWork(iw IWork) error {
	iw.Getup()
	iw.Commute()
	iw.Arrive()
	return nil
}

func (bw *BaseWork) Getup() {
	print("getup.")
}

func (bw *BaseWork) Commute() {
	print("commute.")
}

func (bw *BaseWork) Arrive() {
	print("arrive.")
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

// Drive 具体实现
type Drive struct {
	BaseWork
}

// Commute 重写父类的默认方法
func (d Drive) Commute() {
	print("commute by driving car.")
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

// Bus 具体实现
type Bus struct {
	BaseWork
}

// Commute 重写父类的默认方法
func (b Bus) Commute() {
	print("commute by bus.")
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

func main() {
	bus := &Bus{}
	_ = bus.GotoWork(bus)

	drive := &Drive{}
	_ = drive.GotoWork(drive)

	print("\r\n")

	// 也可通过父类对象调用
	base := &BaseWork{}
	_ = base.GotoWork(&Bus{})
	_ = base.GotoWork(&Drive{})
}

References

guru