亦称: Template Method
模板方法模式是一种行为设计模式, 它在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。
假如你正在开发一款分析公司文档的数据挖掘程序。 用户需要向程序输入各种格式 (PDF、 DOC 或 CSV) 的文档, 程序则会试图从这些文件中抽取有意义的数据, 并以统一的格式将其返回给用户。
该程序的首个版本仅支持 DOC 文件。 在接下来的一个版本中, 程序能够支持 CSV 文件。 一个月后, 你 “教会” 了程序从 PDF 文件中抽取数据。
数据挖掘类中包含许多重复代码。
一段时间后, 你发现这三个类中包含许多相似代码。 尽管这些类处理不同数据格式的代码完全不同, 但数据处理和分析的代码却几乎完全一样。 如果能在保持算法结构完整的情况下去除重复代码, 这难道不是一件很棒的事情吗?
还有另一个与使用这些类的客户端代码相关的问题: 客户端代码中包含许多条件语句, 以根据不同的处理对象类型选择合适的处理过程。 如果所有处理数据的类都拥有相同的接口或基类, 那么你就可以去除客户端代码中的条件语句, 转而使用多态机制来在处理对象上调用函数。
模板方法模式建议将算法分解为一系列步骤, 然后将这些步骤改写为方法, 最后在 “模板方法” 中依次调用这些方法。 步骤可以是 抽象
的, 也可以有一些默认的实现。 为了能够使用算法, 客户端需要自行提供子类并实现所有的抽象步骤。 如有必要还需重写一些步骤 (但这一步中不包括模板方法自身)。
让我们考虑如何在数据挖掘应用中实现上述方案。 我们可为图中的三个解析算法创建一个基类, 该类将定义调用了一系列不同文档处理步骤的模板方法。
模板方法将算法分解为步骤, 并允许子类重写这些步骤, 而非重写实际的模板方法。
首先, 我们将所有步骤声明为 抽象
类型, 强制要求子类自行实现这些方法。 在我们的例子中, 子类中已有所有必要的实现, 因此我们只需调整这些方法的签名, 使之与超类的方法匹配即可。
现在, 让我们看看如何去除重复代码。 对于不同的数据格式, 打开和关闭文件以及抽取和解析数据的代码都不同, 因此无需修改这些方法。 但分析原始数据和生成报告等其他步骤的实现方式非常相似, 因此可将其提取到基类中, 以让子类共享这些代码。
正如你所看到的那样, 我们有两种类型的步骤:
还有另一种名为钩子的步骤。 钩子是内容为空的可选步骤。 即使不重写钩子, 模板方法也能工作。 钩子通常放置在算法重要步骤的前后, 为子类提供额外的算法扩展点。
可对典型的建筑方案进行微调以更好地满足客户需求。
模板方法可用于建造大量房屋。 标准房屋建造方案中可提供几个扩展点, 允许潜在房屋业主调整成品房屋的部分细节。
每个建造步骤 (例如打地基、 建造框架、 建造墙壁和安装水电管线等) 都能进行微调, 这使得成品房屋会略有不同。
抽象
类型, 也可以提供一些默认实现。本例中的模板方法模式为一款简单策略游戏中人工智能的不同分支提供 “框架”。
一款简单游戏的 AI 类。
游戏中所有的种族都有几乎同类的单位和建筑。 因此你可以在不同的种族上复用相同的 AI 结构, 同时还需要具备重写一些细节的能力。 通过这种方式, 你可以重写半兽人的 AI 使其能力更强大, 也可以让人类侧重防守, 还可以禁止怪物建造建筑。 在游戏中新增种族需要创建新的 AI 子类, 还需要重写 AI 基类中所声明的默认方法。
// 抽象类定义了一个模板方法,其中通常会包含某个由抽象原语操作调用组成的算
// 法框架。具体子类会实现这些操作,但是不会对模板方法做出修改。
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
// 怪物不会建造单位。
当你只希望客户端扩展某个特定算法步骤, 而不是整个算法或其结构时, 可使用模板方法模式。
模板方法将整个算法转换为一系列独立的步骤, 以便子类能对其进行扩展, 同时还可让超类中所定义的结构保持完整。
当多个类的算法除一些细微不同之外几乎完全一样时, 你可使用该模式。 但其后果就是, 只要算法发生变化, 你就可能需要修改所有的类。
在将算法转换为模板方法时, 你可将相似的实现步骤提取到超类中以去除重复代码。 子类间各不同的代码可继续保留在子类中。
final
最终修饰模板方法以防止子类对其进行重写。模版方法是一种行为设计模式, 它在基类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。
让我们来考虑一个一次性密码功能 (OTP) 的例子。 将 OTP 传递给用户的方式多种多样 (短信、 邮件等)。 但无论是短信还是邮件, 整个 OTP 流程都是相同的:
后续引入的任何新 OTP 类型都很有可能需要进行相同的上述步骤。
因此, 我们会有这样的一个场景, 其中某个特定操作的步骤是相同的, 但实现方式却可能有所不同。 这正是适合考虑使用模板方法模式的情况。
首先, 我们定义一个由固定数量的方法组成的基础模板算法。 这就是我们的模板方法。 然后我们将实现每一个步骤方法, 但不会改变模板方法。
package main
type IOtp interface {
genRandomOPT(int) string
saveOPTCache(string)
getMessage(string) string
sendNotification(string) error
}
type Otp struct {
iOtp IOtp
}
func (o *Otp) genAndSendOPT(optLength int) error {
opt := o.iOtp.genRandomOPT(optLength)
o.iOtp.saveOPTCache(opt)
message := o.iOtp.getMessage(opt)
if err := o.iOtp.sendNotification(message); err != nil {
return err
}
return nil
}
package main
import (
"fmt"
)
type Sms struct {
Otp
}
func (s *Sms) genRandomOPT(len int) string {
randomOTP := "1234"
fmt.Printf("SMS: generating random otp %s \n", randomOTP)
return randomOTP
}
func (s *Sms) saveOPTCache(otp string) {
fmt.Printf("SMS: saving otp %s", otp)
}
func (s *Sms) getMessage(otp string) string {
return "SMS OTP for login is " + otp
}
func (s *Sms) sendNotification(message string) error {
fmt.Printf("SMS: sending sms: %s\n", message)
return nil
}
package main
import (
"fmt"
)
type Email struct {
Otp
}
func (s *Email) genRandomOPT(len int) string {
randomOTP := "2345"
fmt.Printf("Email: generating random otp %s \n", randomOTP)
return randomOTP
}
func (s *Email) saveOPTCache(otp string) {
fmt.Printf("Email: saving otp %s to cache", otp)
}
func (s *Email) getMessage(otp string) string {
return "Email otp for login is " + otp
}
func (s *Email) sendNotification(message string) error {
fmt.Printf("Email: sending email %s \n", message)
return nil
}
package main
import "fmt"
func main() {
smsOTP := &Sms{}
o := Otp{
iOtp: smsOTP,
}
o.genAndSendOPT(4)
fmt.Println("")
emailOtp := &Email{}
o = Otp{
iOtp: emailOtp,
}
o.genAndSendOPT(4)
}
SMS: generating random otp 1234
SMS: saving otp 1234SMS: sending sms: SMS OTP for login is 1234
Email: generating random otp 2345
Email: saving otp 2345 to cacheEmail: sending email Email otp for login is 2345