转--每周一个GoLang设计模式之组合模式

时间:2022-05-04
本文章向大家介绍转--每周一个GoLang设计模式之组合模式,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

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 : c1c1  intersect with  2Add  &{{c1 12 2 1}} to Childs at position  0c1 's length is  1hello Composite

(大家可以在这里试一下: https://play.golang.org/p/9Cc6HwIqcO )

其实这只是一个很简陋的Composite,里面有很多的地方需要完善,例如我们需要一个全局变量取存储图元的ID数组,还有正确初始化的规则等等。但是关于Composite的基本骨架这里应该都具有了,如果条件允许我会在以后去完善这些方面。