我是设计模式的推崇者,相信一个良好的架构能够给系统的稳定运行和后期维护带来极大的方便,因为最近有时间重新学习GoF的设计模式,于是产生了用Go实现GoF经典设计模式的想法。
这篇文章遵循GoF书中的脉络,本篇是这个系列的第一篇:组合模式(Composite),以后如果在正常工作允许的前提下,应该会每周更新一篇。欢迎大家访问我的博客,代码可以在@Zuozuohao下载。
GoF在第二章通过设计一个Lexi的文档编辑器来介绍设计模式的使用,GoF认为Lexi设计面临七个问题:
1. 文档结构
2. 格式化
3. 修饰用户界面
4. 支持多种视感
5. 支持多种窗口系统
6. 用户操作
7. 拼写检查和连字符
GoF认为Lexi的文档只针对字符、线、多边形和其他图形元素进行处理。但是Lexi的用户通常面临的是文档的物理结构行、列、图形、表和其他子结构,而这些子结构还有他自己的子结构。
Lexi用户界面应该允许直接操作这些子结构,例如用户可以直接操作图表结构,可以引用、移动等等,而不是将图标看作一堆文本和图形。
所以Lexi内部表示应该支持
1. 保持文档的物理结构,即将文本和图标安排到行、列、表等。
2. 可视化生成和显示文档。
3. 根据显示位置来映射文档内部表示的元素。
GoF认为,首先,应该一致的对待文本和图形,例如允许用户在图形嵌入文本,反之亦然。
其次,不应该强调单个元素和元素组的区别,Lexi应该一致的对待简单元组和组合元素。
最后,如果考虑到后续增加文法分析功能,那么简单元素和组合元素的要求会跟第二条产生冲突,因为对简单元素和组合元素的文法分析是不同的(所以设计模式需要权衡)。
递归组合
GoF使用递归组合(Recursive Composition)来表示Lexi图元的层次化结构,首先将字符和图形自左到右排列成文档的一行,然后将多行组合成一列,最后将多列组成一页等等(如下图所示)。
GoF将每个重要元素表示一个对象,从而描述这种层次结构。这些对象不仅包括字符、图形等可见元素,还包括结构化元素,如行和列,对象结构如下图所示。
图元
GoF将文档对象的所有结构定义一个抽象图元(Glyph)。他的子类即定义了基本的图形元素(字符和图像等),还包括结构化元素(行和列),类的继承结构如下图所示。
下表描述了Glyph的基本接口。
Responsibity | Operations |
---|---|
Appearance | Virtual Void Draw(Window*) |
Virtual Void Bounds(Rect&) | |
hit detection | Virtual bool Intersects(Const Point&) |
Structure | Virtual Void Insert(Glyph*, int) |
Virtual Void Remove(Glyph*) | |
Virtual Void Remove(Glyph*) | |
Virtual Glyph* Child(int) | |
Virtual Glyph* Parent(int) |
图元有三种责任,1)他们怎么画出自己,2)他们占用多大空间,3)他们的父图元和子图元是什么。
Glyph子类为了在窗口上呈现自己,必须重写父类Glyph的Draw方法,从而在屏幕窗口上呈现自己。
Bounds方法返回图元占用的矩形区域,Glyph子类需要重写该方法,因为每个对象所占用的面积不同。
Intersects判断一个指定点是否与图元相交,用以确定用户在Lexi界面点击位置的图元或者图元结构。
Remove方法会移出一个对象的子图元。
Child方法返回给定的图元的子图元。
Parent方法返回对象的父图元。
以上是GoF关于Lexi文档编辑器应该遵循的基本设计,总结起来应该是两个要点:
1.层次化的对象结构,包括基本图元和组合图元
2.通用的接口设计
下面我们来尝试用Golang来实现这个基本设计模式。
Golang图元类型
Lexi文档编辑器应该包括以下图元类型Character、Rectangle、Row和Column等等,为了方便阅读(主要是真的不想敲那么多字)我们只选择Character、Rectangle、Row三种对象进行实现,其他的图元类型可以自己尝试一下。
限于篇幅原因(其实我真的不想码字,嘿嘿)这里只是选取了部分GoF定义的图元和接口,请谅解。
Golang图元类型接口实现*
正如类图所设计的那样,三者都包含Draw和Intersects方法,组合图元Row多出一个插入子图元的Insert接口。
因此我们设计一个通用的Appearancer接口用来描述通用接口类型,代码如下:
type Appearancer interface {
Draw(elemet Appearancer)
Intersect(point int)
SetParent(parentID int)
}
图元除了具有名称属性之外,还应该具有一个表征身份的ID,用以区分不同图元,所以Glyph、Character、Rectangle和Row类型设计如下:
type Glyph struct {
Name string
Position int
ID int //ID must > 0
ParentID int //if ParentID equal 0, the Glyph has no parents
}
type Character struct {
Glyph
}
type Rectangle struct {
Glyph
}
type Row struct {
Glyph
Childs []Appearancer
}
下面是Appearancer接口的实现部分,通用接口的工作基本可以在Glyph类型中完成:
func (g *Glyph) Draw(elemet Appearancer) {
fmt.Println("I am a ", reflect.TypeOf(elemet), ":", g.Name)
}
func (g *Glyph) Intersect(point int) {
if g.Position == point {
fmt.Println(g.Name, " is far away from ", point)
} else {
fmt.Println(g.Name, " intersect with ", point)
}
}
func (g *Glyph) SetParent(parentID int) {
g.ParentID = parentID
}
func (r *Row) Insert(child Appearancer, position int) {
index := r.insertInRightPlace(child, position)
child.SetParent(r.ID)
fmt.Println("Add ", child, "to Childs at position ", index)
fmt.Println(r.Name, "'s length is ", len(r.Childs))
}
func (parent *Row) insertInRightPlace(child Appearancer, position int) int {
insertedPosition := 0
childsLength := len(parent.Childs)
if position > (childsLength - 1) {
parent.Childs = append(parent.Childs, child)
insertedPosition = childsLength
} else {
parent.Childs = append(parent.Childs[position:position], child)
insertedPosition = position
}
return insertedPosition
}
然后就可以直接向Row里面插入图元了,代码如下:
func main() {
c1 := &Row{Glyph{"c1", 12, 1, 0}, []Appearancer{}}
c1.Draw(c1)
c1.Intersect(2)
c1.Insert(&Character{Glyph{"c1", 12, 2, 0}}, 3)
fmt.Println("hello Composite")
}
输出:
I am a *main.Row : c1
c1 intersect with 2
Add &{{c1 12 2 1}} to Childs at position 0
c1 's length is 1
hello Composite
(大家可以在这里试一下: https://play.golang.org/p/9Cc6HwIqcO )
其实这只是一个很简陋的Composite,里面有很多的地方需要完善,例如我们需要一个全局变量取存储图元的ID数组,还有正确初始化的规则等等。但是关于Composite的基本骨架这里应该都具有了,如果条件允许我会在以后去完善这些方面。
非常感谢您读完这篇冗长的文章,如有错误之处请指出,我会尽快修改,谢谢!