模板模式定义算法骨架,使用上有两个特征,一是要继承算法骨架,达到复用的目的;二是具体的算法步骤在子类中实现,达到扩展的目的。
模板模式:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
UML:
模板模式的UML图几乎是最简单的了。
**模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。**TemplateMethod是算法骨架,PrimitiveOperation1和PrimitiveOperation2是骨架中的某些步骤。
在模板模式经典的实现中,模板方法定义为 final,可以避免被子类重写。需要子类重写的方法定义为 abstract,可以强迫子类去实现。
以前用这种定义好算法骨架,具体实现在不同子类的方案时,一般使用的是工厂方法加代理模式。工厂方法能够提供更多的灵活性,但如果一个算法骨架中有10个具体算法,总不能让工厂生产10个不同的对象吧。所以如果算法骨架中有多个具体算法,而这些算法又是高内聚的,用模板模式就很合适。
业务开发场景中,模板模式使用频率并不高,但是在框架方面,还是使用的比较频繁的。
先查看了Gin源码Gin源码剖析,发现用的不是模板模式,其实完全没用啥设计模式,算是用了里氏替换原则。
主框架有Handler接口,用于做路由解析、对应逻辑执行与返回。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Gin中的engin实现该接口
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
之所以说没有使用模板模式,主要是因为并没有继承算法框架。
后来想想Go语言没有继承,没法实现经典款模板方法。虽然可以用组合实现模板方法,但无法控制对算法框架的重写、也无法强迫子类重写算法实现,感觉价值不大。有很多更好的方案能够实现目的。
虽然没什么特别好的case,但是代码还是要写一下的。参考大话设计模式,写一个试卷场景吧。试卷内容确定,但试卷答案不定。
package main
import "fmt"
/**
* @Description: 试卷
*/
type Examination struct {
//函数变量,回答问题1
Answer1 func()
//函数变量,回答问题2
Answer2 func()
}
/**
* @Description: 问题列表,也是算法骨架
* @receiver e
*/
func (e *Examination) Questions() {
fmt.Println("第一题:谁是最帅的人?")
e.Answer1()
fmt.Println("第二题:生活的意义是什么?")
e.Answer2()
}
/**
* @Description: 真正做试卷
*/
type ExamplationDo struct {
Examination
}
/**
* @Description: 写答案1
* @receiver d
*/
func (d *ExamplationDo) Answer1() {
fmt.Println("答案:我自己")
}
/**
* @Description: 写答案2
* @receiver d
*/
func (d *ExamplationDo) Answer2() {
fmt.Println("答案:躺平")
}
func main() {
e := &ExamplationDo{}
//需要对父类函数进行赋值
e.Examination.Answer1 = e.Answer1
e.Examination.Answer2 = e.Answer2
e.Questions()
}
输出:
➜ myproject go run main.go
第一题:谁是最帅的人?
答案:我自己
第二题:生活的意义是什么?
答案:躺平
上面的代码使用一些技巧才能实现模板模式。父类Examination实现了算法骨架,同时包含两个函数变量Answer1和Answer2,代表算法实现。子类ExamplationDo实现了Answer1和Answer2,并将这两个函数赋值给父类的函数变量。
举个 ,假设我现在要做一个短信推送的系统,那么需要
我们可以发现,在发送短信的时候由于不同的供应商调用的接口不同,所以会有一些实现上的差异,但是他的算法(业务逻辑)是固定的
package template
import "fmt"
// ISMS ISMS
type ISMS interface {
send(content string, phone int) error
}
// SMS 短信发送基类
type sms struct {
ISMS
}
// Valid 校验短信字数
func (s *sms) Valid(content string) error {
if len(content) > 63 {
return fmt.Errorf("content is too long")
}
return nil
}
// Send 发送短信
func (s *sms) Send(content string, phone int) error {
if err := s.Valid(content); err != nil {
return err
}
// 调用子类的方法发送短信
return s.send(content, phone)
}
// TelecomSms 走电信通道
type TelecomSms struct {
*sms
}
// NewTelecomSms NewTelecomSms
func NewTelecomSms() *TelecomSms {
tel := &TelecomSms{}
// 这里有点绕,是因为 go 没有继承,用嵌套结构体的方法进行模拟
// 这里将子类作为接口嵌入父类,就可以让父类的模板方法 Send 调用到子类的函数
// 实际使用中,我们并不会这么写,都是采用组合+接口的方式完成类似的功能
tel.sms = &sms{ISMS: tel}
return tel
}
func (tel *TelecomSms) send(content string, phone int) error {
fmt.Println("send by telecom success")
return nil
}
package template
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_sms_Send(t *testing.T) {
tel := NewTelecomSms()
err := tel.Send("test", 1239999)
assert.NoError(t, err)
}
模板模式有两大作用:复用和扩展。其中,复用指的是,所有的子类可以复用父类中提供的模板方法的代码。扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。