简述
在面向对象编程中,有两个常见的对象设计方法,组合和继承,两者都可以解决代码复用的问题,但是使用后者时容易出现继承层次过深,对象关系过于复杂的副作用,从而导致代码的可维护性变差。因此,一个经典的面向对象设计原则是:组合优于继承。
我们都知道,组合所表示的语义为“has-a”,也就是部分和整体的关系,最经典的组合模式描述如下:
将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
Go语言天然就支持了组合模式,而且从它不支持继承关系的特点来看,Go也奉行了组合优于继承的原则,鼓励大家在进行程序设计时多采用组合的方法。Go实现组合模式的方式有两种,分别是直接组合(Direct Composition)和嵌入组合(Embedding Composition),下面我们一起探讨这两种不同的实现方法。
Go实现
直接组合(Direct Composition)的实现方式类似于Java/C++,就是将一个对象作为另一个对象的成员属性。
一个典型的实现如《使用Go实现GoF的23种设计模式(一)》中所举的例子,一个Message结构体,由Header和Body所组成。那么Message就是一个整体,而Header和Body则为消息的组成部分。
type Message struct {
Header *Header
Body *Body
}
现在,我们来看一个稍微复杂一点的例子,同样考虑上一篇文章中所描述的插件架构风格的消息处理系统。前面我们用抽象工厂模式解决了插件加载的问题,通常,每个插件都会有一个生命周期,常见的就是启动状态和停止状态,现在我们使用组合模式来解决插件的启动和停止问题。
首先给Plugin接口添加几个生命周期相关的方法:
复制代码
package plugin
...
// 插件运行状态
type Status uint8
const (
Stopped Status = iota
Started
)
type Plugin interface {
// 启动插件
Start()
// 停止插件
Stop()
// 返回插件当前的运行状态
Status() Status
}
// Input、Filter、Output三类插件接口的定义跟上一篇文章类似
// 这里使用Message结构体替代了原来的string,使得语义更清晰
type Input interface {
Plugin
Receive() *msg.Message
}
type Filter interface {
Plugin
Process(msg *msg.Message) *msg.Message
}
type Output interface {
Plugin
Send(msg *msg.Message)
}
复制代码
对于插件化的消息处理系统而言,一切皆是插件,因此我们将Pipeine也设计成一个插件,实现Plugin接口:
复制代码
package pipeline
...
// 一个Pipeline由input、filter、output三个Plugin组成
type Pipeline struct {
status plugin.Status
input plugin.Input
filter plugin.Filter
output plugin.Output
}
func (p *Pipeline) Exec() {
msg := p.input.Receive()
msg = p.filter.Process(msg)
p.output.Send(msg)
}
// 启动的顺序 output -> filter -> input
func (p *Pipeline) Start() {
p.output.Start()
p.filter.Start()
p.input.Start()
p.status = plugin.Started
fmt.Println("Hello input plugin started.")
}
// 停止的顺序 input -> filter -> output
func (p *Pipeline) Stop() {
p.input.Stop()
p.filter.Stop()
p.output.Stop()
p.status = plugin.Stopped
fmt.Println("Hello input plugin stopped.")
}
func (p *Pipeline) Status() plugin.Status {
return p.status
}
复制代码
一个Pipeline由Input、Filter、Output三类插件组成,形成了“部分-整体”的关系,而且它们都实现了Plugin接口,这就是一个典型的组合模式的实现。Client无需显式地启动和停止Input、Filter和Output插件,在调用Pipeline对象的Start和Stop方法时,Pipeline就已经帮你按顺序完成对应插件的启动和停止。
相比于上一篇文章,在本文中实现Input、Filter、Output三类插件时,需要多实现3个生命周期的方法。还是以上一篇文章中的HelloInput、UpperFilter和ConsoleOutput作为例子,具体实现如下:
复制代码
package plugin
...
type HelloInput struct {
status Status
}
func (h HelloInput) Receive() msg.Message {
// 如果插件未启动,则返回nil
if h.status != Started {
fmt.Println("Hello input plugin is not running, input nothing.")
return nil
}
return msg.Builder().
WithHeaderItem("content", "text").
WithBodyItem("Hello World").
Build()
}
func (h *HelloInput) Start() {
h.status = Started
fmt.Println("Hello input plugin started.")
}
func (h *HelloInput) Stop() {
h.status = Stopped
fmt.Println("Hello input plugin stopped.")
}
func (h *HelloInput) Status() Status {
return h.status
}
package plugin
...
type UpperFilter struct {
status Status
}
func (u UpperFilter) Process(msg msg.Message) *msg.Message {
if u.status != Started {
fmt.Println("Upper filter plugin is not running, filter nothing.")
return msg
}
for i, val := range msg.Body.Items {
msg.Body.Items[i] = strings.ToUpper(val)
}
return msg
}
func (u *UpperFilter) Start() {
u.status = Started
fmt.Println("Upper filter plugin started.")
}
func (u *UpperFilter) Stop() {
u.status = Stopped
fmt.Println("Upper filter plugin stopped.")
}
func (u *UpperFilter) Status() Status {
return u.status
}
package plugin
...
type ConsoleOutput struct {
status Status
}
func (c ConsoleOutput) Send(msg msg.Message) {
if c.status != Started {
fmt.Println("Console output is not running, output nothing.")
return
}
fmt.Printf("Output:\n\tHeader:%+v, Body:%+v\n", msg.Header.Items, msg.Body.Items)
}
func (c *ConsoleOutput) Start() {
c.status = Started
fmt.Println("Console output plugin started.")
}
func (c *ConsoleOutput) Stop() {
c.status = Stopped
fmt.Println("Console output plugin stopped.")
}
func (c *ConsoleOutput) Status() Status {
return c.status
}
复制代码
测试代码如下:
复制代码
package test
...
func TestPipeline(t *testing.T) {
p := pipeline.Of(pipeline.DefaultConfig())
p.Start()
p.Exec()
p.Stop()
}
// 运行结果
=== RUN TestPipeline
Console output plugin started.
Upper filter plugin started.
Hello input plugin started.
Pipeline started.
Output:
Header:map[content:text], Body:[HELLO WORLD]
Hello input plugin stopped.
Upper filter plugin stopped.
Console output plugin stopped.
Hello input plugin stopped.
--- PASS: TestPipeline (0.00s)
PASS
复制代码
组合模式的另一种实现,嵌入组合(Embedding Composition),其实就是利用了Go语言的匿名成员特性,本质上跟直接组合是一致的。
还是以Message结构体为例,如果采用嵌入组合,则看起来像是这样:
type Message struct {
Header
Body
}
// 使用时,Message可以引用Header和Body的成员属性,例如:
msg := &Message{}
msg.SrcAddr = "192.168.0.1"