2014年11月16日星期日

Go语言设计模式实践:组合(Composite) - newgame

本邮件内容由第三方提供,如果您不想继续收到该邮件,可 点此退订
Go语言设计模式实践:组合(Composite) - newgame  阅读原文»

关于本系列

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

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

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

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

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

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

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

姑且把KFC里的食物认为是菜单项,一份套餐是菜单。菜单和菜单项有一些公有属性:名字、描述、价格、都能被购买等,所以正如GoF所说,我们需要一致性地使用它们。它们的层次结构体现在一个菜单里会包含多个菜单项或菜单,其价格是所有子项的和。嗯,这个例子其实不是很恰当,不能很好的体现菜单包含菜单的情况,所以我多定义了一个"超值午餐"菜单,其中包含若干个套餐。

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

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杯

奥尔良烤鸡腿饭套餐, 供应时间:09:15--22:44, ¥30.50
------------------------
主食, ¥15.00
-- 新奥尔良烤鸡腿饭1份
小吃, ¥11.00
-- 新奥尔良烤翅2块
饮料, ¥4.50
-- 芙蓉荟蔬汤1份

面向对象实现

先说明一下:Go语言不是面向对象语言,实际上只有struct而没有类或对象。但是为了说明方便,后面我会使用这个术语来表示struct的定义,用对象这个术语来表示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, ¥%.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...)
}

func (m *Menu) Child(idx int) MenuComponent {
return m.children
}

其中Price()统计所有子项的Price后加和,Print()输出自身的信息后依次输出所有子项的信息。另注意Remove()的实现(从slice中删除一项)。

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

  1. MenuItemMenu中都有name、description这两个属性和方法,重复写两遍明显冗余。如果使用其它任何面向对象语言,这两个属性和方法都应该移到基类中实现。可是Go没有继承,这可真是坑爹。
  2. 这里我们真正实现了用户一致性访问了吗?显然没有,当使用者拿到一个MenuComponent后,依然要知道其类型后才能正确使用,假如不加判断在MenuItem使用Add()等未实现的方法就会产生panic。类似地,我们大可以把文件夹/文件都抽象成"文件系统节点",可以读取名字,可以计算占用空间,但是一旦我们想往"文件系统节点"中添加子节点时,还是必须得判断它到底是不是文件夹。
  3. 接着第2条继续思考:产生某种一致性访问现象的本质原因是什么?一种观点:MenuMenuItem某种本质上是(is-a)同一个事物(MenuComponent),所以可以对它们一致性访问;另一种观点:MenuMenuItem是两个不同的事物,只是恰巧有一些相同的属性,所以可以对它们一致性访问。

用组合代替继承

前面说到Go语言没有继承,本来属于基类的name和description不能放到基类中实现。其实只要转换一下思路,这个问题是很容易用组合解决的。如果我们认为MenuMenuItem本质上是两个不同的事物,只是恰巧有(has-a)一些相同的属性,那么将相同的属性抽离出来,再分别组合进两者,问题就迎刃而解了。

先看抽离出来的属性:

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

// ... 方法略 ...

Go语言中善用组合有助于表达数据结构的意图。特别是当一个比较复杂的对象同时处理几方面的事情时,将对象拆成独立的几个部分再组合到一起,会非常清晰优雅。例如上面的MenuItem就是描述+价格,Menu就是描述+子菜单。

其实对于Menu,更好的做法是把childrenAdd() 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...)
}

func (m *Menu) Child(idx int) MenuComponent {
return m.children
}

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不依赖于接口。

好了,与之前不同,现在我们不是先定义接口再具体实现,因为struct不依赖于接口,所以我们直接实现具体功能。先是MenuDescMenuItem,注意现在NewMenuItem的返回值类型是*MenuItem

type MenuDesc struct {
内容营销:让你的产品与用户对话 - know@more  阅读原文»

内容将会影响消费者的体验、行动计划和谈论的话题;我们的产品不够好,往往不是UI不够简洁、美观或用户体验差,而是在于产品的内容无法打动并留住用户,更不用谈与用户产生情感上的互动...

看文章标题,你就知道,我所谈的跟营销有关。纵观整个互联网世界,关于运营和营销方面的文章铺天盖地,而并我不是做运营或营销的,更谈不上是这方面的专家,只能从一个很小的点谈下自己的一些想法。

情景化带入

上面是“本来生活网”的一个名为:回家吃饭的专题页截图;现在做O2O生鲜电商的网站在逐渐增多,就是看淘宝、京东等平台型的商城,其实真正能吸引并留住用户,不在乎哪个UI更漂亮,当然毋庸置疑好的UI是用户的第一印象也很关键,而是在于哪个内容更能打动用户——即内容营销。

“回家吃饭”这个专题,我第一次浏览这个网站时就被其吸引,也促使我一直在关注这个网站,就在这点儿上它就比其它同类型的网站略胜一筹。为何?回家吃饭这个场景设定,可以让用户不由自主、很自然的被情景化带入——今天是老公生日,我要早点儿回家给他做顿满满都是爱的生日晚餐;今天是周末,儿女都回家,一家人难得有时间聚到一起,我要好好的露一手,做顿丰盛的饭菜...,无论你是哪种身份,每个人都有回家吃饭的时候;回家吃饭自然而然就会考虑要做点儿什么好吃的,糖醋里脊、金枪鱼牛油果沙拉、或是茶树菇煲鸡汤...,等一下,这些都经常吃,我要做点儿特别的,让大家眼前一亮,那就学做几道新菜品吧。好了,通过我这么一说,想必你也应该大概知道这个专题页好在哪儿了。

从产品角度来说,它很好的把握并满足了用户想“回家吃饭”这一场景下的需求,而不是像其它的网站,很直白的把一个个(做菜的原料)商品摆在用户面前卖,僵硬的商品,让用户有距离感;且在用户没有事先想好要做什么菜时,Ta还要有些费力的去想应该在这众多的商品中挑选哪些,用户都比较懒,就是换做我们在这种情况下,也会有些茫然、或是选择困难症。

简而言之,这个专题页好在:以情景化带入,给你推荐几道不错的菜品,并教你怎么做,而且我(网站)想给你或做的就是这些,如果你喜欢这道菜,你就会买其所需的原料。这就好比销售高手,Ta会跟用户沟通了解后,提供帮助、满足用户的需要,不会直截了当的给用户推销产品,急切的想让用户购买,而是隐藏本意,真诚的跟用户交流,解决其遇到的问题。

让内容鲜活

(图一)

(图二)

(图三)

上面是三个不同商城里水果列表页面的截图,你仔细对比看后,应该不难发现——三张图片里的水果分别是:保鲜膜包裹、敞露(部分切开)、敞露并切开,那么现在问题来了,排除你对水果的喜好和图片的质量,在你看完这三张图片后,你最想购买哪张图片里的水果?我相信大多数人都会选择第三张图片,因为它能在第一时间勾起你的购买欲望。写到这儿,我还忍不住要多看一会儿这张图片,唉,实在是让人太想吃了(呵呵)...

最后,还希望做营销或运营方面的朋友,对文章中的不到之处请多多包涵,并给与指点一二。

文章推荐(关注订阅号,回复数字即可查看)

    • 34. 产品聚焦和市场细分

    • 44. 思考、质疑之解惑

    • 43. 产品经理应聘之感受漫谈

    • 47. 由 100offer 谈创新

我的微信订阅号:zxcknowmore,你的关注与支持,是我多写博文的动力!


本文链接:内容营销:让你的产品与用户对话,转载请注明。

阅读更多内容

没有评论:

发表评论