[Golang]设计模式实践:组合(Composite)
??
关于本系列这个系列首先是关于Go语言实践的。在项目中实际使用Go语言也有段时间了,一个体会就是不论是官方文档、图书还是网络资料,关于Go语言惯用法(idiom)的介绍都比较少,基本只能靠看标准库源代码自己琢磨,所以我特别想在这方面有一些收集和总结。 然后这个系列也是关于设计模式的。虽然Go语言不是一门面向对象编程语言,但是很多面向对象设计模式所要解决的问题是在程序设计中客观存在的。不管用什么语言,总是要面对和解决这些问题的,只是解决的思路和途径会有所不同。所以我想就以经典的设计模式作为切入点来展开这个系列,毕竟大家对设计模式都很熟悉了,可以避免无中生有想出一些蹩脚的应用场景。 本系列的具体主题会比较灵活,计划主要包括这些方面的话题:
GoF对组合模式的定义是,将对象组合成树形结构以表示“部分整体”的层次结构,组合模式使得用户对单个对象和组合对象的使用具有一致性。 对于这句话我是有异议的,这里先卖个关子,我们先从实际例子说起。 组合模式的例子大家都见得很多了,比如文件系统(文件/文件夹)、GUI窗口(Frame/Control)、菜单(菜单/菜单项)等等,我这里也举个菜单的例子,不过不是操作系统里的菜单,是真正的菜单,KFC的…… 姑且把KFC里的食物认为是 用代码归纳总结一下,最终我们的调用代码是这样的: func main() { menu1 := NewMenu("培根鸡腿燕麦堡套餐","供应时间:09:15--22:44") menu1.Add(NewMenuItem("主食","培根鸡腿燕麦堡1个",11.5)) menu1.Add(NewMenuItem("小吃","玉米沙拉1份",5.0)) menu1.Add(NewMenuItem("饮料","九珍果汁饮料1杯",6.5)) menu2 := NewMenu("奥尔良烤鸡腿饭套餐","供应时间:09:15--22:44") menu2.Add(NewMenuItem("主食","新奥尔良烤鸡腿饭1份",15.0)) menu2.Add(NewMenuItem("小吃","新奥尔良烤翅2块",11.0)) menu2.Add(NewMenuItem("饮料","芙蓉荟蔬汤1份",4.5)) all := NewMenu("超值午餐","周一至周五有售") all.Add(menu1) all.Add(menu2) all.Print() } 得到的输出如下: 超值午餐,周一至周五有售,¥53.50 ------------------------ 培根鸡腿燕麦堡套餐,供应时间:09:15--22:44,¥23.00 ------------------------ 主食,¥11.50 -- 培根鸡腿燕麦堡1个 小吃,¥5.00 -- 玉米沙拉1份 饮料,¥6.50 -- 九珍果汁饮料1杯 奥尔良烤鸡腿饭套餐,¥30.50 ------------------------ 主食,¥15.00 -- 新奥尔良烤鸡腿饭1份 小吃,¥11.00 -- 新奥尔良烤翅2块 饮料,¥4.50 -- 芙蓉荟蔬汤1份 面向对象实现先说明一下:Go语言不是面向对象语言,实际上只有struct而没有类或对象。但是为了说明方便,后面我会使用 按照惯例,先使用经典的面向对象来分析。首先我们需要定义菜单和菜单项的抽象基类,这样使用者就可以只依赖于接口了,于是实现使用上的一致性。 Go语言中没有继承,所以我们把抽象基类定义为接口,后面会由菜单和菜单项实现具体功能: type MenuComponent interface { Name() string Description() string Price() float32 Print() Add(MenuComponent) Remove(int) Child(int) MenuComponent } 菜单项的实现: type MenuItem struct { name string description string price float32 } func NewMenuItem(name,description string,price float32) MenuComponent { return &MenuItem{ name: name,description: description,price: price,} } func (m *MenuItem) Name() string { return m.name } func (m *MenuItem) Description() string { return m.description } func (m *MenuItem) Price() float32 { return m.price } func (m *MenuItem) Print() { fmt.Printf(" %s,¥%.2fn",m.name,m.price) fmt.Printf(" -- %sn",m.description) } func (m *MenuItem) Add(MenuComponent) { panic("not implement") } func (m *MenuItem) Remove(int) { panic("not implement") } func (m *MenuItem) Child(int) MenuComponent { panic("not implement") } 有两点请留意一下。
下面是菜单的实现: type Menu struct { name string description string children []MenuComponent } func NewMenu(name,description string) MenuComponent { return &Menu{ name: name,} } func (m *Menu) Name() string { return m.name } func (m *Menu) Description() string { return m.description } func (m *Menu) Price() (price float32) { for _,v := range m.children { price += v.Price() } return } func (m *Menu) Print() { fmt.Printf("%s,%s,m.description,m.Price()) fmt.Println("------------------------") for _,v := range m.children { v.Print() } fmt.Println() } func (m *Menu) Add(c MenuComponent) { m.children = append(m.children,c) } func (m *Menu) Remove(idx int) { m.children = append(m.children[:idx],m.children[idx+1:]...) } func (m *Menu) Child(idx int) MenuComponent { return m.children[idx] } 其中 好,现在针对这份实现思考下面3个问题。
用组合代替继承前面说到Go语言没有继承,本来属于基类的name和description不能放到基类中实现。其实只要转换一下思路,这个问题是很容易用组合解决的。如果我们认为 先看抽离出来的属性: type MenuDesc struct { name string description string } func (m *MenuDesc) Name() string { return m.name } func (m *MenuDesc) Description() string { return m.description } 改写 type MenuItem struct { MenuDesc price float32 } func NewMenuItem(name,price float32) MenuComponent { return &MenuItem{ MenuDesc: MenuDesc{ name: name,},price: price,} } // ... 方法略 ... 改写 type Menu struct { MenuDesc children []MenuComponent } func NewMenu(name,description string) MenuComponent { return &Menu{ MenuDesc: MenuDesc{ name: name,} } // ... 方法略 ... Go语言中善用组合有助于表达数据结构的意图。特别是当一个比较复杂的对象同时处理几方面的事情时,将对象拆成独立的几个部分再组合到一起,会非常清晰优雅。例如上面的 其实对于 type MenuGroup struct { children []MenuComponent } func (m *Menu) Add(c MenuComponent) { m.children = append(m.children,m.children[idx+1:]...) } func (m *Menu) Child(idx int) MenuComponent { return m.children[idx] } type Menu struct { MenuDesc MenuGroup } func NewMenu(name,} } Go语言的思维方式以下是本文的重点。使用Go语言开发项目2个多月,最大的感触就是:学习Go语言一定要转变思维方式,转变成功则其乐无穷,不能及时转变会发现自己处处碰壁。 下面让我们用真正Go的方式来实现KFC菜单。首先请默念三遍:没有继承,没有继承,没有继承;没有基类,没有基类,没有基类;接口只是函数签名的集合,接口只是函数签名的集合,接口只是函数签名的集合;struct不依赖于接口,struct不依赖于接口,struct不依赖于接口。 好了,与之前不同,现在我们不是先定义接口再具体实现,因为struct不依赖于接口,所以我们直接实现具体功能。先是 type MenuDesc struct { name string description string } func (m *MenuDesc) Name() string { return m.name } func (m *MenuDesc) Description() string { return m.description } type MenuItem struct { MenuDesc price float32 } func NewMenuItem(name,price float32) *MenuItem { return &MenuItem{ MenuDesc: MenuDesc{ name: name,} } func (m *MenuItem) Price() float32 { return m.price } func (m *MenuItem) Print() { fmt.Printf(" %s,m.description) } 接下来是 type MenuComponent interface { } type MenuGroup struct { children []MenuComponent } func (m *Menu) Add(c MenuComponent) { m.children = append(m.children,m.children[idx+1:]...) } func (m *Menu) Child(idx int) MenuComponent { return m.children[idx] } 最后是 type Menu struct { MenuDesc MenuGroup } func NewMenu(name,description string) *Menu { return &Menu{ MenuDesc: MenuDesc{ name: name,} } func (m *Menu) Price() (price float32) { for _,v := range m.children { v.Print() } fmt.Println() } 在实现 type MenuComponent interface { Price() float32 Print() } 最后观察 比较与思考前后两份代码差异其实很小:
从思路上看,差异很大却也有些微妙:
注意第一份实现中, 另外,从 if m,ok := all.Child(1).(*Menu); ok { m.Add(NewMenuItem("玩具","Hello Kitty",5.0)) } 清晰明了,如果某child是一个 更进一步,这里我们对类型的要求其实并没有那么强,并不需要它一定要是 type Group interface { Add(c MenuComponent) Remove(idx int) Child(idx int) MenuComponent } 前面的添加子项的代码改成这样: if m,ok := all.Child(1).(Group); ok { m.Add(NewMenuItem("玩具",5.0)) } 再考虑一下“购买”这个操作,面向对象的实现中,购买的类型是 type Product interface { Price() float32 } 于是购买操作不仅可应用于 总结最后总结一下我的思考,欢迎各位讨论或抨击:
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |