严格来说 Go 并不特别适用 Java 的设计模式。但是我们主要目的是掌握方法以降低耦合、提高复用性。
单例模式
一句话总结:利用包域变量保存实例,利用 once.Do
保证线程安全。原理是互斥锁。
输入:空 输出:全局唯一的单例
var once sync.Once
var dbConn *DB
func GetConnection() *DB {
if dbConn == nil {
once.Do(
func() {
dbConn = NewConnection()
}
)
}
return dbConn
}
抽象工厂模式
简单工厂模式
其实就是判断参数后区别创建对象,你可以视其为一个对象类选择器。里面往往是一堆条件语句。
输入:简单的参数 输出:复杂的对象。且返回的任何对象都属于同一接口。
var plugins []IPlugin
const userType = {
AdminType
MemberType
}
func main() {
var user IUser = NewUser(userType)
}
func NewUser(t userType) IUser {
switch t {
case AdminType:
return &Admin{}
}
case MemberType:
return &Member{}
// ...
}
依赖关系:
main --> NewUser, userType, IUser
NewUser -> IUser, Admin, Member
这种情况下,具体的类对于用户依旧是可见的,并没有改善耦合度,只不过是把创建的代码提取出个方法而已。
工厂方法模式
工厂方法则是将创建过程下放到具体的类中:
var plugins []IPlugin
const userType = {
AdminType
MemberType
}
func main() {
var user = NewUser(userType)
}
func NewUser(t userType) IUser{
switch t {
case AdminType:
return NewAdminType()
}
case MemberType:
return NewMemberType()
// ...
}
依赖关系:
main --> NewUser, userType, IUser
NewUser -> IUser, NewAdminType, NewMemberType
NewAdminType -> Admin
NewMemberType -> Member
可以看到,在 Go 中使用工厂方法模式,主程序间接依赖了所有。依旧不会降低耦合度,只是在简单工厂之上又做了一层方法提取,把创建过程下放罢了。
抽象工厂方式
很简单,就是把工厂也变成接口。
var plugins []IPlugin
const userType = {
AdminType
MemberType
}
type ICreator func (t userType) IUser
func main() {
var creator ICreator = NewUser()
creator.createUser(t)
}
func NewUser(t userType) IUser {
switch t {
case AdminType:
return NewAdminType()
}
case MemberType:
return NewMemberType()
// ...
}
依赖关系
main --> ICreator, IUser, (NewUser)
NewUserCreator -> NewUser, userType
NewUser -> NewAdminType, NewMemberType
NewAdminType -> Admin
NewMemberType -> Member
可以看到,抽象工厂主程序终于不直接依赖工厂了,耦合度终于有了实质性的降低。
但是仍旧需要约定大家都知道 userType。
生成器模式
一句话总结:通过返回生成器自身实现链式调用,通过完成函数返回最终对象。
例子:
builder := &ServerBuilder{}
builder
.SetHost("127.0.0.1")
.AddPort("80")
.AddPort("443", keys)
.Run()
原型模式
说白了就是实现克隆接口,从而即使不依赖具体实例的类也可以克隆它。
适配器模式
一句话总结:其实就是加一个中间层,从而让两边兼容。
典型例子:现有不同格式的配置文件类库,通过一个适配器使其能够自动调用适合的类库,读取出配置信息
桥接模式
其实还是接口。让一个函数的行为依赖于接口而不是实现,从而可以任意调整实现。
比如:我们需要一个函数检验用户输入的验证码是否正确,最初实现:
func check(userId int64, captcha string) bool {
return myDb
.from("tbl_captcha")
.where("userId = ?", userId)
.where("captcha = ?", captcha)
.count() > 0
}
check(req.userId, req.captcha)
后来发现查数据库性能太差,需要能在用数据库和用 redis 之间切换,可以改成:
type IChecker func (int64, string) bool
var checker []IChecker= []{redisCaptchaCheck, dbCaptchaCheck}
func check(userId int64, captcha string) bool {
checker[0](userId, captcha) // or checker[1](userId, captcha)
}
func redisCaptchaCheck(userId int64, captcha string) bool {
return redisDb.get(tbl_captcha + "_" + userId + "_" + captcha).exists()
}
func dbCaptchaCheck(userId int64, captcha string) bool {
return myDb
.from("tbl_captcha")
.where("userId = ?", userId)
.where("captcha = ?", captcha)
.count() > 0
}
这样,check 函数就不依赖于具体的检查函数,而是依赖于 IChecker 接口。具体的函数完全可以在外部定义。
组件模式
说白了就是含有自指指针的结构。
包括链表、二叉树就是组合模式的体现。它具有分形的特点。
type Node struct {
Data int
Left *Node
Right *Node
}
更加业务化的例子就是 Vue、React 里的组件。
外观模式
这也能整出个设计模式我是没料到的。其实就是提取代码,写流水账,把细节分到别处。
享元模式
这也能整出个设计模式…… 说白了就是共享指针。
举个例子吧,做 3D 程序时需要保存大量的顶点数据,而同类顶点往往只有坐标是不同的,其它数据都是相同的。享元模式相同的不各自存放,而是全部存放在一处,从而节省内存,并保证一致性。
链表模式
下面的几个模式本质上就是个链表,说句挨骂的话,结构没啥区别。不过由于用途不同,被赋予不同的名称和意义。
装饰器模式
说白了也是建造者,只不过建造者也同时是本体。
自我建造(修饰)的过程可以用函数实现
editor.getSelected()
.setBold()
.setFontSize(13)
也可以用结构实现
var sel = editor.getSelected()
sel = BoldSetter(sel)
sel = FontSizeSetter(sel, 13)
典型的例子:中间件模式
var app = &App{}
app = &CorsMiddleware{in: app}
app = &AuthMiddleware{in: app}
代理模式
代理模式和装饰器模式是同构的,所以就结构而言,没有任何区别。
只是,代理模式更强调一个局外人接管了流程中的环节。装饰器模式更强调原本的功能得到了增强:
代理:
var app = &App{}
appWithAuth = &AuthMiddleware{next: app}
appWithCors = &CorsMiddleware{next: appWithAuth}
装饰器:
var app = &App{}
app = &AuthMiddleware{next: app}
app = &CorsMiddleware{next: app}
责任链模式
责任链又是一个是和装饰器同构的模式。你可以想象一组过滤器,每个过滤器能过滤掉一种物质。
流水线模式
其实就是责任链模式。不过一般说流水线,是线性的链,而责任链还包括树状的链。
命令模式
说白了就是多处重复的代码提取出来成为一个命令,大家要用就只要调这个命令就行了。出了 BUG 也只要改命令的实现部分。
在 WPF 中运用广泛。另外 React 的 Redux,Vue 的 Vuex,其 Action 就是命令。
迭代器模式
假设你有一个 100GB 的文件,你电脑的内存只有 32G,总不能全部读进来遍历吧?那么你可以逐个读取。这就是所谓迭代器。
package main
import (
"bufio"
"fmt"
"strings"
)
func main() {
in := `{"sample":"json string"}`
s := bufio.NewScanner(strings.NewReader(in))
s.Split(bufio.ScanRunes)
for s.Scan() {
fmt.Println(s.Text())
}
}
调停模式
又叫中介模式,其实就是层次化管理。
比如你有一个程序要依赖 A,B,C…Z,那你可以分出子程序 0,负责管理 A~E, 1 负责管理 F~K…… 这样,需要直接负责的就少了。
和外观模式是同构的。
备忘录模式
说白了就是私自保存历史,从而可以恢复历史状态。
观察者模式
也叫发布订阅(pubsub)。这个模式非常有用。
成员:
- 事件管理者
- 管理通知
- 订阅者
- 接收通知
- 发布者
- 发布通知