这一节我们继续了解创建型模式的原型模式。
我们仍然从实际案例入手来了解原型模式。例如假设有一幅画,其中包含有天空、草地、很多棵树,描述这幅画的结构定义如下:
type Tree struct {
Height string
Color string
}
type Pic struct {
sky string
Grass string
Trees []Tree
}
此时,如果已经存在一个对象为一副春天的图画,而我再想生成一副秋天的图画。秋天和春天的实物是不变的,有天空、草地、相同数目的树,仅仅只是颜色变化了。在程序的角度该如何处理呢?
我们还是从反例着手,我们先看第一个反例,而且是错了的反例,使用 = 对新对象的赋值,然后对对象的成员使用赋值的方式改变,这里对slice的处理还会影响旧的slice,详细原因可见go语言系列5 - 你不得不知道slice的那些事:
func main() {
trees := []Tree{
{"1m", "Green"},
{"3m", "Green"},
{"5m", "Green"},
}
springPic := Pic{
Sky: "Blue",
Grass: "Green",
Trees: trees,
}
var autumnPic Pic = springPic
autumnPic.Sky = "gray"
autumnPic.Grass = "Yello"
for i := range autumnPic.Trees {
autumnPic.Trees[i].Color = "Yello"
}
fmt.Println(springPic)
fmt.Println(autumnPic)
}
上面代码的输出中 springPic的树颜色也会更改为 Yello了。正确代码如下:
func main() {
trees := []Tree{
{"1m", "Green"},
{"3m", "Green"},
{"5m", "Green"},
}
springPic := Pic{
Sky: "Blue",
Grass: "Green",
Trees: trees,
}
var autumnPic Pic = springPic
autumnPic.Sky = "gray"
autumnPic.Grass = "Yello"
newTrees := make([]Tree, len(autumnPic.Trees))
for i, t := range autumnPic.Trees {
newTrees[i] = Tree{t.Height, "Yello"}
}
autumnPic.Trees = newTrees
fmt.Println(springPic)
fmt.Println(autumnPic)
}
这样对 Trees 的赋值就不会影响 原有的对象了。但是我们考虑下,如果又要生成一幅冬天的画,那还是得把上述代码再写一遍吗?我们可以把这部分复制的过程交给 Pic的成员变量去处理:
func (p Pic) Clone() Pic {
newP := Pic{
Sky: p.Sky,
Grass: p.Grass,
Trees: make([]Tree, len(p.Trees)),
}
for i, t := range p.Trees {
newP.Trees[i] = t
}
return newP
}
而生成一副秋天的画的过程如下:
autumnPic := springPic.Clone()
autumnPic.Grass = "Yello"
autumnPic.Sky = "Gray"
for i := range autumnPic.Trees {
autumnPic.Trees[i].Color = "Yello"
}
我们看下这样的实现有什么好处?
由于对象的拷贝成员函数中处理了,对于使用者无需关注对象的类型是什么,需要复制哪些成员。
我们示例代码中对象的属性都是public类型(大写开头),在struct外部可访问,如果是private,在成员函数外部还无法对属性直接赋值,而成员内部是可以赋值的。
如果成员的属性特别多,复制过程交给成员函数处理,使用者无需关注那些不变的元素,示例中关注颜色属性就行。
代码复用,每复制一个对象,无需把复制的代码又写一遍。
我们看代码中的Trees 属性 其实仍然是有优化空间的,我们现在的处理在 Pic 的成员内部还是有关注 Trees 是什么类型进行处理。这里我们可以对Trees 再增加Clone方法,使代码使用原型模式更彻底。
type Trees []Tree
type Pic struct {
Sky string
Grass string
Trees Trees
}
func (t Trees) Clone() Trees {
newtrees := make([]Tree, len(t))
for i := range newtrees {
newtrees[i] = t[i]
}
return newtrees
}
func (p Pic) Clone() Pic {
newP := Pic{
Sky: p.Sky,
Grass: p.Grass,
Trees: p.Trees.Clone(),
}
return newP
}
这样Pic的Clone方法可以优化成如下:
func (p Pic) Clone() Pic {
newP := Pic{
Sky: p.Sky,
Grass: p.Grass,
Trees: p.Trees.Clone(),
}
return newP
}
接下来我们看下在创建型设计模式中最常见的单例模式,这个模式可以不用多介绍。在使用数据库连接等保证只有一个实例的场景经常使用到,我们直接看代码:
type Conn struct {
}
var (
conn *Conn
once sync.Once
)
func GetConn() *Conn {
once.Do(func() {
conn = &Conn{}
})
return conn
}
单例模式中的单例是何时生成的呢?有两种模式主动生成或者懒加载。上述代码示例使用的懒加载形式,在使用时才会去生成对象。这里大家可能会通过 conn 是否为nil来判断,但是Go的 sync 包的once就支持使方法只执行一次的对象实现,可直接使用。
顺便再说下主动的方式,大家可能会使用 init 来初始化conn,这里我不太建议交给Go去决定何时初始化,由于包引用关系在大型系统中非常复杂了,很难去阅读出对象的初始化顺序了,一旦某个对象依赖其他的对象,很可能由于顺序问题出错。我建议如下代码示例中主动的去初始化conn:
func initConn(){
conn = &Conn{}
}
func GetConn() *Conn {
return conn
}
由程序的使用者(开发人员决定何时初始化conn),而不是交给Go去决定何时初始化,尽可能的减少出错的情况。
有关创建型设计模式在Go上的应用就讲解到这里了,接下来的章节我们将对剩余的设计模式进行讲解。
往期回顾:
go语言系列4 - Goroutine、channel的使用还有话说
go语言系列7 - Go defer优化之open_coded