关于本系列

这个系列首先是关于Go语言实践的。在项目中实际使用Go语言也有段时间了,一个体会就是不论是官方文档、图书还是网络资料,关于Go语言惯用法(idiom)的介绍都比较少,基本只能靠看标准库源代码自己琢磨,所以我特别想在这方面有一些收集和总结。

然后这个系列也是关于设计模式的。虽然Go语言不是一门面向对象编程语言,但是很多面向对象设计模式所要解决的问题是在程序设计中客观存在的。不管用什么语言,总是要面对和解决这些问题的,只是解决的思路和途径会有所不同。所以我想就以经典的设计模式作为切入点来展开这个系列,毕竟大家对设计模式都很熟悉了,可以避免无中生有想出一些蹩脚的应用场景。

本系列的具体主题会比较灵活,计划主要包括这些方面的话题:

  1. Go语言惯用法。
  2. 设计模式的实现。特别是引入了闭包,协程,DuckType等语言特性后带来的变化。
  3. 设计模式思想的探讨。会有一些吐槽。

GoF对组合模式的定义是,将对象组合成树形结构以表示“部分整体”的层次结构,组合模式使得用户对单个对象和组合对象的使用具有一致性

对于这句话我是有异议的,这里先卖个关子,我们先从实际例子说起。

组合模式的例子大家都见得很多了,比如文件系统(文件/文件夹)、GUI窗口(Frame/Control)、菜单(菜单/菜单项)等等,我这里也举个菜单的例子,不过不是操作系统里的菜单,是真正的菜单,KFC的……

菜单项菜单

用代码归纳总结一下,最终我们的调用代码是这样的:

得到的输出如下:

面向对象实现

类对象

按照惯例,先使用经典的面向对象来分析。首先我们需要定义菜单和菜单项的抽象基类,这样使用者就可以只依赖于接口了,于是实现使用上的一致性。

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, ¥%.2f\n", m.name, m.price)
	fmt.Printf("    -- %s\n", 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")
}

有两点请留意一下。

  1. NewMenuItem()创建的是MenuItem,但返回的是抽象的接口MenuComponent。(面向对象中的多态)
  2. 因为MenuItem是叶节点,无法提供Add() Remove() Child()这三个方法的实现,所以若被调用会panic。

下面是菜单的实现:

type Menu struct {
	name        string
	description string
	children    []MenuComponent
}

func NewMenu(name, description string) MenuComponent {
	return &Menu{
		name:        name,
		description: description,
	}
}

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, ¥%.2f\n", m.name, 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]
}
Price()PricePrint()Remove()

好,现在针对这份实现思考下面3个问题。

MenuItemMenuMenuComponentMenuItemAdd()MenuMenuItemMenuComponentMenuMenuItem

用组合代替继承

MenuMenuItem

先看抽离出来的属性:

type MenuDesc struct {
	name        string
	description string
}

func (m *MenuDesc) Name() string {
	return m.name
}

func (m *MenuDesc) Description() string {
	return m.description
}
MenuItem
type MenuItem struct {
	MenuDesc
	price float32
}

func NewMenuItem(name, description string, price float32) MenuComponent {
	return &MenuItem{
		MenuDesc: MenuDesc{
			name:        name,
			description: description,
		},
		price: price,
	}
}

// ... 方法略 ...
Menu
type Menu struct {
	MenuDesc
	children []MenuComponent
}

func NewMenu(name, description string) MenuComponent {
	return &Menu{
		MenuDesc: MenuDesc{
			name:        name,
			description: description,
		},
	}
}

// ... 方法略 ...
MenuItemMenu
MenuchildrenAdd()Remove()Child()Menu
type MenuGroup struct {
	children []MenuComponent
}

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]
}

type Menu struct {
	MenuDesc
	MenuGroup
}

func NewMenu(name, description string) MenuComponent {
	return &Menu{
		MenuDesc: MenuDesc{
			name:        name,
			description: description,
		},
	}
}

Go语言的思维方式

以下是本文的重点。使用Go语言开发项目2个多月,最大的感触就是:学习Go语言一定要转变思维方式,转变成功则其乐无穷,不能及时转变会发现自己处处碰壁。

下面让我们用真正Go的方式来实现KFC菜单。首先请默念三遍:没有继承,没有继承,没有继承;没有基类,没有基类,没有基类;接口只是函数签名的集合,接口只是函数签名的集合,接口只是函数签名的集合;struct不依赖于接口,struct不依赖于接口,struct不依赖于接口。

MenuDescMenuItemNewMenuItem*MenuItem
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, description string, price float32) *MenuItem {
	return &MenuItem{
		MenuDesc: MenuDesc{
			name:        name,
			description: description,
		},
		price: price,
	}
}

func (m *MenuItem) Price() float32 {
	return m.price
}

func (m *MenuItem) Print() {
	fmt.Printf("  %s, ¥%.2f\n", m.name, m.price)
	fmt.Printf("    -- %s\n", m.description)
}
MenuGroupMenuGroupchildrenMenuGroupchildrenchildreninterface{}
type MenuComponent interface {
}

type MenuGroup struct {
	children []MenuComponent
}

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]
}
Menu
type Menu struct {
	MenuDesc
	MenuGroup
}

func NewMenu(name, description string) *Menu {
	return &Menu{
		MenuDesc: MenuDesc{
			name:        name,
			description: description,
		},
	}
}

func (m *Menu) Price() (price float32) {
	for _, v := range m.children {
		price += v.Price()
	}
	return
}

func (m *Menu) Print() {
	fmt.Printf("%s, %s, ¥%.2f\n", m.name, m.description, m.Price())
	fmt.Println("------------------------")
	for _, v := range m.children {
		v.Print()
	}
	fmt.Println()
}
MenuMenuchildrenPrice()Print()MenuComponent
type MenuComponent interface {
	Price() float32
	Print()
}
MenuItemMenuMenuComponentMenuchildren

比较与思考

前后两份代码差异其实很小:

  1. 第二份实现的接口简单一些,只有两个函数。
  2. New函数返回值的类型不一样。

从思路上看,差异很大却也有些微妙:

childrenMenuComponentchildrenMenuComponent
MenuComponentAdd()Remove()Child()
MenuPrice()Print()MenuComponentMenu
if m, ok := all.Child(1).(*Menu); ok {
	m.Add(NewMenuItem("玩具", "Hello Kitty", 5.0))
}
MenuAdd()
MenuMenuComponent
type Group interface {
	Add(c MenuComponent)
	Remove(idx int)
	Child(idx int) MenuComponent
}

前面的添加子项的代码改成这样:

if m, ok := all.Child(1).(Group); ok {
	m.Add(NewMenuItem("玩具", "Hello Kitty", 5.0))
}
MenuComponentMenuMenuItemPrice()
type Product interface {
    Price() float32
}
MenuMenuItemPrice()

总结

最后总结一下我的思考,欢迎各位讨论或抨击:

  1. 在组合模式中,一致性访问是个伪需求。一致性访问不是我们在设计时需要去满足的需求,而是当不同实体具有相同属性时自然产生的效果。上面的例子中,我们创建的是menu和MenuItem两种不同的类型,但由于它们具有相同属性,我们能以相同的方式取价格,取描述,加入menu成为子项。
  2. Go语言中的多态不体现在对象创建阶段,而体现在对象使用阶段,合理使用“小接口”能显著减少系统耦合度。

PS. 本文所涉及的三份完整代码,我放在play.golang.org上了:(需FQ)