结合Golang聊聊23种设计模式—行为型(1)
我们常把 23 种经典的设计模式分为三类:创建型、结构型、行为型。前面我们已经学习了创建型和结构型,从今天起,我们开始学习行为型设计模式。我们知道,创建型设计模式主要解决“对象的创建”问题,结构型设计模式主要解决“类或对象的组合或组装”问题,那行为型设计模式主要解决的就是“类或对象之间的交互”问题。
行为型设计模式比较多,有 11 个,几乎占了 23 种经典设计模式的一半。它们分别是:观察者模式、模板模式、策略模式、职责链模式、状态模式、迭代器模式、访问者模式、备忘录模式、命令模式、解释器模式、中介模式。
观察者模式 观察者模式(Observer Design Pattern)也被称为发布订阅模式(Publish-Subscribe Design Pattern)。在 GoF 的《设计模式》一书中,它的定义是这样的:Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.翻译成中文就是:在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
一般情况下,被依赖的对象叫作被观察者(Observable),依赖的对象叫作观察者(Observer)。不过,在实际的项目开发中,这两种对象的称呼是比较灵活的,有各种不同的叫法,比如:Subject-Observer、Publisher-Subscriber、Producer-Consumer、EventEmitter-EventListener、Dispatcher-Listener。不管怎么称呼,只要应用场景符合刚刚给出的定义,都可以看作观察者模式。
为什么要用观察者模式 实际上,设计模式要干的事情就是解耦。创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦,具体到观察者模式,它是将观察者和被观察者代码解耦。借助设计模式,我们利用更好的代码结构,将一大坨代码拆分成职责更单一的小类,让其满足开闭原则、高内聚松耦合等特性,以此来控制和应对代码的复杂性,提高代码的可扩展性。
观察者模式的应用场景非常广泛,小到代码层面的解耦,大到架构层面的系统解耦,再或者一些产品的设计思路,都有这种模式的影子,比如,邮件订阅、RSS Feeds,本质上都是观察者模式。不同的应用场景和需求下,这个模式也有截然不同的实现方式,有同步阻塞的实现方式,也有异步非阻塞的实现方式;有进程内的实现方式,也有跨进程的实现方式。
还有消息队列本身也是一种辅助实现观察者模式的工具。
代码示例 在电商网站中, 商品时不时地会出现缺货情况。 可能会有客户对于缺货的特定商品表现出兴趣。 这一问题有三种解决方案:
客户以一定的频率查看商品的可用性。
电商网站向客户发送有库存的所有新商品。
客户只订阅其感兴趣的特定商品, 商品可用时便会收到通知。 同时, 多名客户也可订阅同一款产品。
选项 3 是最具可行性的, 这其实就是观察者模式的思想。 观察者模式的主要组成部分有:
会在有任何事发生时发布事件的主体。
订阅了主体事件并会在事件发生时收到通知的观察者。
subject.go: 主体1 2 3 4 5 6 7 package maintype Subject interface { register(observer Observer) deregister(observer Observer) notifyAll() }
item.go: 具体主体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 package mainimport "fmt" type Item struct { observerList []Observer name string inStock bool } func newItem (name string ) *Item { return &Item{ name: name, } } func (i *Item) updateAvailability () { fmt.Printf("Item %s is now in stock\n" , i.name) i.inStock = true i.notifyAll() } func (i *Item) register (o Observer) { i.observerList = append (i.observerList, o) } func (i *Item) deregister (o Observer) { i.observerList = removeFromslice(i.observerList, o) } func (i *Item) notifyAll () { for _, observer := range i.observerList { observer.update(i.name) } } func removeFromslice (observerList []Observer, observerToRemove Observer) []Observer { observerListLength := len (observerList) for i, observer := range observerList { if observerToRemove.getID() == observer.getID() { observerList[observerListLength-1 ], observerList[i] = observerList[i], observerList[observerListLength-1 ] return observerList[:observerListLength-1 ] } } return observerList }
observer.go: 观察者1 2 3 4 5 6 package maintype Observer interface { update(string ) getID() string }
customer.go: 具体观察者1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "fmt" type Customer struct { id string } func (c *Customer) update (itemName string ) { fmt.Printf("Sending email to customer %s for item %s\n" , c.id, itemName) } func (c *Customer) getID () string { return c.id }
main.go: 客户端代码1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainfunc main () { shirtItem := newItem("Nike Shirt" ) observerFirst := &Customer{id: "abc@gmail.com" } observerSecond := &Customer{id: "xyz@gmail.com" } shirtItem.register(observerFirst) shirtItem.register(observerSecond) shirtItem.updateAvailability() }
output.txt: 执行结果1 2 3 Item Nike Shirt is now in stock Sending email to customer abc@gmail.com for item Nike Shirt Sending email to customer xyz@gmail.com for item Nike Shirt
场景 当一个对象状态的改变需要改变其他对象, 或实际对象是事先未知的或动态变化的时, 可使用观察者模式。
当你使用图形用户界面类时通常会遇到一个问题。 比如, 你创建了自定义按钮类并允许客户端在按钮中注入自定义代码, 这样当用户按下按钮时就会触发这些代码。
观察者模式允许任何实现了订阅者接口的对象订阅发布者对象的事件通知。 你可在按钮中添加订阅机制, 允许客户端通过自定义订阅类注入自定义代码。
当应用中的一些对象必须观察其他对象时, 可使用该模式。 但仅能在有限时间内或特定情况下使用。
订阅列表是动态的, 因此订阅者可随时加入或离开该列表。
优点
开闭原则 。 你无需修改发布者代码就能引入新的订阅者类 (如果是发布者接口则可轻松引入发布者类)。
你可以在运行时建立对象之间的联系。
缺点
中介模式 中介模式的英文翻译是 Mediator Design Pattern。在 GoF 中的《设计模式》一书中,它是这样定义的:Mediator pattern defines a separate (mediator) object that encapsulates the interaction between a set of objects and the objects delegate their interaction to a mediator object instead of interacting with each other directly.翻译成中文就是:中介模式定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。
为什么使用中介模式 中介模式的设计思想跟中间层很像,通过引入中介这个中间层,将一组对象之间的交互关系(或者说依赖关系)从多对多(网状关系)转换为一对多(星状关系)。原来一个对象要跟 n 个对象交互,现在只需要跟一个中介对象交互,从而最小化对象之间的交互关系,降低了代码的复杂度,提高了代码的可读性和可维护性。
代码示例 概念示例 中介者模式的一个绝佳例子就是火车站交通系统。 两列火车互相之间从来不会就站台的空闲状态进行通信。 stationManager
车站经理可充当中介者, 让平台仅可由一列入场火车使用, 而将其他火车放入队列中等待。 离场火车会向车站发送通知, 便于队列中的下一列火车进站。
train.go: 组件1 2 3 4 5 6 7 package maintype Train interface { arrive() depart() permitArrival() }
passengerTrain.go: 具体组件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 package mainimport "fmt" type PassengerTrain struct { mediator Mediator } func (g *PassengerTrain) arrive () { if !g.mediator.canArrive(g) { fmt.Println("PassengerTrain: Arrival blocked, waiting" ) return } fmt.Println("PassengerTrain: Arrived" ) } func (g *PassengerTrain) depart () { fmt.Println("PassengerTrain: Leaving" ) g.mediator.notifyAboutDeparture() } func (g *PassengerTrain) permitArrival () { fmt.Println("PassengerTrain: Arrival permitted, arriving" ) g.arrive() }
freightTrain.go: 具体组件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 package mainimport "fmt" type FreightTrain struct { mediator Mediator } func (g *FreightTrain) arrive () { if !g.mediator.canArrive(g) { fmt.Println("FreightTrain: Arrival blocked, waiting" ) return } fmt.Println("FreightTrain: Arrived" ) } func (g *FreightTrain) depart () { fmt.Println("FreightTrain: Leaving" ) g.mediator.notifyAboutDeparture() } func (g *FreightTrain) permitArrival () { fmt.Println("FreightTrain: Arrival permitted" ) g.arrive() }
1 2 3 4 5 6 package maintype Mediator interface { canArrive(Train) bool notifyAboutDeparture() }
stationManager.go: 具体中介者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 package maintype StationManager struct { isPlatformFree bool trainQueue []Train } func newStationManger () *StationManager { return &StationManager{ isPlatformFree: true , } } func (s *StationManager) canArrive (t Train) bool { if s.isPlatformFree { s.isPlatformFree = false return true } s.trainQueue = append (s.trainQueue, t) return false } func (s *StationManager) notifyAboutDeparture () { if !s.isPlatformFree { s.isPlatformFree = true } if len (s.trainQueue) > 0 { firstTrainInQueue := s.trainQueue[0 ] s.trainQueue = s.trainQueue[1 :] firstTrainInQueue.permitArrival() } }
main.go: 客户端代码1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainfunc main () { stationManager := newStationManger() passengerTrain := &PassengerTrain{ mediator: stationManager, } freightTrain := &FreightTrain{ mediator: stationManager, } passengerTrain.arrive() freightTrain.arrive() passengerTrain.depart() }
output.txt: 执行结果1 2 3 4 5 PassengerTrain: Arrived FreightTrain: Arrival blocked, waiting PassengerTrain: Leaving FreightTrain: Arrival permitted FreightTrain: Arrived
场景 当一些对象和其他对象紧密耦合以致难以对其进行修改时, 可使用中介者模式。
该模式让你将对象间的所有关系抽取成为一个单独的类, 以使对于特定组件的修改工作独立于其他组件。
当组件因过于依赖其他组件而无法在不同应用中复用时, 可使用中介者模式。
应用中介者模式后, 每个组件不再知晓其他组件的情况。 尽管这些组件无法直接交流, 但它们仍可通过中介者对象进行间接交流。 如果你希望在不同应用中复用一个组件, 则需要为其提供一个新的中介者类。
如果为了能在不同情景下复用一些基本行为, 导致你需要被迫创建大量组件子类时, 可使用中介者模式。
由于所有组件间关系都被包含在中介者中, 因此你无需修改组件就能方便地新建中介者类以定义新的组件合作方式。
优点
单一职责原则 。 你可以将多个组件间的交流抽取到同一位置, 使其更易于理解和维护。
开闭原则 。 你无需修改实际组件就能增加新的中介者。
你可以减轻应用中多个组件间的耦合情况。
你可以更方便地复用各个组件。
缺点
访问者模式 访问者模式允许一个或者多个操作应用到一组对象上,设计意图是解耦操作和对象本身,保持类职责单一、满足开闭原则以及应对代码的复杂性。
为什么要使用访问者模式 访问者模式针对的是一组类型不同的对象,尽管这组对象的类型是不同的,但是,它们继承相同的父类或者实现相同的接口。在不同的应用场景下,我们需要对这组对象进行一系列不相关的业务操作(抽取文本、压缩等),但为了避免不断添加功能导致类不断膨胀,职责越来越不单一,以及避免频繁地添加功能导致的频繁代码修改,我们使用访问者模式,将对象与操作解耦,将这些业务操作抽离出来,定义在独立细分的访问者类(Extractor、Compressor)中。
访问者模式建议将新行为放入一个名为访问者 的独立类中, 而不是试图将其整合到已有类中。 现在, 需要执行操作的原始对象将作为参数被传递给访问者中的方法, 让方法能访问对象所包含的一切必要数据。
代码示例 访问者模式允许你在结构体中添加行为, 而又不会对结构体造成实际变更。 假设你是一个代码库的维护者, 代码库中包含不同的形状结构体, 如:
上述每个形状结构体都实现了通用形状接口。
在公司员工开始使用你维护的代码库时, 你就会被各种功能请求给淹没。 让我们来看看其中比较简单的请求: 有个团队请求你在形状结构体中添加 getArea
获取面积行为。
解决这一问题的办法有很多。
第一个选项便是将 getArea
方法直接添加至形状接口, 然后在各个形状结构体中进行实现。 这似乎是比较好的解决方案, 但其代价也比较高。 作为代码库的管理员, 相信你也不想在每次有人要求添加另外一种行为时就去冒着风险改动自己的宝贝代码。 不过, 你也一定想让其他团队的人还是用一用自己的代码库。
第二个选项是请求功能的团队自行实现行为。 然而这并不总是可行, 因为行为可能会依赖于私有代码。
第三个方法就是使用访问者模式来解决上述问题。 首先定义一个如下访问者接口:
1 2 3 4 5 type visitor interface { visitForSquare(square) visitForCircle(circle) visitForTriangle(triangle) }
我们可以使用 visitForSquare(square)
、 visitForCircle(circle)
以及 visitForTriangle(triangle)
函数来为方形、 圆形以及三角形添加相应的功能。
你可能在想, 为什么我们不再访问者接口里面使用单一的 visit(shape)
方法呢? 这是因为 Go 语言不支持方法重载, 所以你无法以相同名称、 不同参数的方式来使用方法。
好了, 第二项重要的工作是将 accept
接受方法添加至形状接口中。
所有形状结构体都需要定义此方法, 类似于:
1 2 3 func (obj *square) accept (v visitor) { v.visitForSquare(obj) }
等等, 我刚才是不是提到过, 我们并不想修改现有的形状结构体? 很不幸, 在使用访问者模式时, 我们必须要修改形状结构体。 但这样的修改只需要进行一次。
如果添加任何其他行为, 比如 getNumSides
获取边数和 getMiddleCoordinates
获取中点坐标 , 我们将使用相同的 accept(v visitor)
函数, 而无需对形状结构体进行进一步的修改。
最后, 形状结构体只需要修改一次, 并且所有未来针对不同行为的请求都可以使用相同的 accept 函数来进行处理。 如果团队成员请求 getArea
行为, 我们只需简单地定义访问者接口的具体实现, 并在其中编写面积的计算逻辑即可。
shape.go: 元件1 2 3 4 5 6 package maintype Shape interface { getType() string accept(Visitor) }
square.go: 具体元件1 2 3 4 5 6 7 8 9 10 11 12 13 package maintype Square struct { side int } func (s *Square) accept (v Visitor) { v.visitForSquare(s) } func (s *Square) getType () string { return "Square" }
circle.go: 具体元件1 2 3 4 5 6 7 8 9 10 11 12 13 package maintype Circle struct { radius int } func (c *Circle) accept (v Visitor) { v.visitForCircle(c) } func (c *Circle) getType () string { return "Circle" }
rectangle.go: 具体元件1 2 3 4 5 6 7 8 9 10 11 12 13 14 package maintype Rectangle struct { l int b int } func (t *Rectangle) accept (v Visitor) { v.visitForrectangle(t) } func (t *Rectangle) getType () string { return "rectangle" }
visitor.go: 访问者1 2 3 4 5 6 7 package maintype Visitor interface { visitForSquare(*Square) visitForCircle(*Circle) visitForrectangle(*Rectangle) }
areaCalculator.go: 具体访问者1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport ( "fmt" ) type AreaCalculator struct { area int } func (a *AreaCalculator) visitForSquare (s *Square) { fmt.Println("Calculating area for square" ) } func (a *AreaCalculator) visitForCircle (s *Circle) { fmt.Println("Calculating area for circle" ) } func (a *AreaCalculator) visitForrectangle (s *Rectangle) { fmt.Println("Calculating area for rectangle" ) }
middleCoordinates.go: 具体访问者1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport "fmt" type MiddleCoordinates struct { x int y int } func (a *MiddleCoordinates) visitForSquare (s *Square) { fmt.Println("Calculating middle point coordinates for square" ) } func (a *MiddleCoordinates) visitForCircle (c *Circle) { fmt.Println("Calculating middle point coordinates for circle" ) } func (a *MiddleCoordinates) visitForrectangle (t *Rectangle) { fmt.Println("Calculating middle point coordinates for rectangle" ) }
main.go: 客户端代码1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport "fmt" func main () { square := &Square{side: 2 } circle := &Circle{radius: 3 } rectangle := &Rectangle{l: 2 , b: 3 } areaCalculator := &AreaCalculator{} square.accept(areaCalculator) circle.accept(areaCalculator) rectangle.accept(areaCalculator) fmt.Println() middleCoordinates := &MiddleCoordinates{} square.accept(middleCoordinates) circle.accept(middleCoordinates) rectangle.accept(middleCoordinates) }
output.txt: 执行结果1 2 3 4 5 6 7 Calculating area for square Calculating area for circle Calculating area for rectangle Calculating middle point coordinates for square Calculating middle point coordinates for circle Calculating middle point coordinates for rectangle
场景 如果你需要对一个复杂对象结构 (例如对象树) 中的所有元素执行某些操作, 可使用访问者模式。
访问者模式通过在访问者对象中为多个目标类提供相同操作的变体, 让你能在属于不同类的一组对象上执行同一操作。
可使用访问者模式来清理辅助行为的业务逻辑。
该模式会将所有非主要的行为抽取到一组访问者类中, 使得程序的主要类能更专注于主要的工作。
当某个行为仅在类层次结构中的一些类中有意义, 而在其他类中没有意义时, 可使用该模式。
你可将该行为抽取到单独的访问者类中, 只需实现接收相关类的对象作为参数的访问者方法并将其他方法留空即可。
优点
开闭原则 。 你可以引入在不同类对象上执行的新行为, 且无需对这些类做出修改。
单一职责原则 。 可将同一行为的不同版本移到同一个类中。
访问者对象可以在与各种对象交互时收集一些有用的信息。 当你想要遍历一些复杂的对象结构 (例如对象树), 并在结构中的每个对象上应用访问者时, 这些信息可能会有所帮助。
缺点
每次在元素层次结构中添加或移除一个类时, 你都要更新所有的访问者。
在访问者同某个元素进行交互时, 它们可能没有访问元素私有成员变量和方法的必要权限。
模板模式 模板模式的原理与实现模板模式,全称是模板方法设计模式,英文是 Template Method Design Pattern。在 GoF 的《设计模式》一书中,它是这么定义的:Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.
翻译成中文就是:模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。
为什么要使用模版模式 模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
模板模式有两大作用:复用和扩展。其中,复用指的是,所有的子类可以复用父类中提供的模板方法的代码。扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能
代码示例 让我们来考虑一个一次性密码功能 (OTP) 的例子。 将 OTP 传递给用户的方式多种多样 (短信、 邮件等)。 但无论是短信还是邮件, 整个 OTP 流程都是相同的:
生成随机的 n 位数字。
在缓存中保存这组数字以便进行后续验证。
准备内容。
发送通知。
后续引入的任何新 OTP 类型都很有可能需要进行相同的上述步骤。
因此, 我们会有这样的一个场景, 其中某个特定操作的步骤是相同的, 但实现方式却可能有所不同。 这正是适合考虑使用模板方法模式的情况。
首先, 我们定义一个由固定数量的方法组成的基础模板算法。 这就是我们的模板方法。 然后我们将实现每一个步骤方法, 但不会改变模板方法。
otp.go: 模板方法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 package maintype IOtp interface { genRandomOTP(int ) string saveOTPCache(string ) getMessage(string ) string sendNotification(string ) error } type Otp struct { iOtp IOtp } func (o *Otp) genAndSendOTP (otpLength int ) error { otp := o.iOtp.genRandomOTP(otpLength) o.iOtp.saveOTPCache(otp) message := o.iOtp.getMessage(otp) err := o.iOtp.sendNotification(message) if err != nil { return err } return nil }
sms.go: 具体实施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 package mainimport "fmt" type Sms struct { Otp } func (s *Sms) genRandomOTP (len int ) string { randomOTP := "1234" fmt.Printf("SMS: generating random otp %s\n" , randomOTP) return randomOTP } func (s *Sms) saveOTPCache (otp string ) { fmt.Printf("SMS: saving otp: %s to cache\n" , 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 }
email.go: 具体实施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 package mainimport "fmt" type Email struct { Otp } func (s *Email) genRandomOTP (len int ) string { randomOTP := "1234" fmt.Printf("EMAIL: generating random otp %s\n" , randomOTP) return randomOTP } func (s *Email) saveOTPCache (otp string ) { fmt.Printf("EMAIL: saving otp: %s to cache\n" , 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 }
main.go: 客户端代码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 package mainimport "fmt" func main () { smsOTP := &Sms{} o := Otp{ iOtp: smsOTP, } o.genAndSendOTP(4 ) fmt.Println("" ) emailOTP := &Email{} o = Otp{ iOtp: emailOTP, } o.genAndSendOTP(4 ) }
output.txt: 执行结果1 2 3 4 5 6 7 SMS: generating random otp 1234 SMS: saving otp: 1234 to cache SMS: sending sms: SMS OTP for login is 1234 EMAIL: generating random otp 1234 EMAIL: saving otp: 1234 to cache EMAIL: sending email: EMAIL OTP for login is 1234
场景 当你只希望客户端扩展某个特定算法步骤, 而不是整个算法或其结构时, 可使用模板方法模式。
模板方法将整个算法转换为一系列独立的步骤, 以便子类能对其进行扩展, 同时还可让超类中所定义的结构保持完整。
当多个类的算法除一些细微不同之外几乎完全一样时, 你可使用该模式。 但其后果就是, 只要算法发生变化, 你就可能需要修改所有的类。
在将算法转换为模板方法时, 你可将相似的实现步骤提取到超类中以去除重复代码。 子类间各不同的代码可继续保留在子类中。
优点
你可仅允许客户端重写一个大型算法中的特定部分, 使得算法其他部分修改对其所造成的影响减小。
你可将重复代码提取到一个超类中。
缺点
部分客户端可能会受到算法框架的限制。
通过子类抑制默认步骤实现可能会导致违反里氏替换原则 。
模板方法中的步骤越多, 其维护工作就可能会越困难。