Go语言设计模式之结构型模式


Posted in Golang onJune 22, 2021
目录
  • 一、组合模式(Composite Pattern)
    • 1.1、简述
    • 1.2、Go实现
  • 二、适配器模式(Adapter Pattern)
    • 2.1、简述
    • 2.2、Go实现
  • 三、桥接模式(Bridge Pattern)
    • 3.1、简述
    • 3.2、Go实现
  • 四、总结

 

一、组合模式(Composite Pattern)

Go语言设计模式之结构型模式

 

1.1、简述

在面向对象编程中,有两个常见的对象设计方法,组合和继承,两者都可以解决代码复用的问题,但是使用后者时容易出现继承层次过深,对象关系过于复杂的副作用,从而导致代码的可维护性变差。因此,一个经典的面向对象设计原则是:组合优于继承。

我们都知道,组合所表示的语义为“has-a”,也就是部分和整体的关系,最经典的组合模式描述如下:

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

Go语言天然就支持了组合模式,而且从它不支持继承关系的特点来看,Go也奉行了组合优于继承的原则,鼓励大家在进行程序设计时多采用组合的方法。Go实现组合模式的方式有两种,分别是直接组合(Direct Composition)和嵌入组合(Embedding Composition),下面我们一起探讨这两种不同的实现方法。

 

1.2、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"

 

二、适配器模式(Adapter Pattern)

Go语言设计模式之结构型模式

 

2.1、简述

适配器模式是最常用的结构型模式之一,它让原本因为接口不匹配而无法一起工作的两个对象能够一起工作。在现实生活中,适配器模式也是处处可见,比如电源插头转换器,可以让英式的插头工作在中式的插座上。适配器模式所做的就是将一个接口Adaptee,通过适配器Adapter转换成Client所期望的另一个接口Target来使用,实现原理也很简单,就是Adapter通过实现Target接口,并在对应的方法中调用Adaptee的接口实现。

一个典型的应用场景是,系统中一个老的接口已经过时即将废弃,但因为历史包袱没法立即将老接口全部替换为新接口,这时可以新增一个适配器,将老的接口适配成新的接口来使用。适配器模式很好的践行了面向对象设计原则里的开闭原则(open/closed principle),新增一个接口时也无需修改老接口,只需多加一个适配层即可。

 

2.2、Go实现

继续考虑上一节的消息处理系统例子,目前为止,系统的输入都源自于HelloInput,现在假设需要给系统新增从Kafka消息队列中接收数据的功能,其中Kafka消费者的接口如下:

package kafka
...
type Records struct {
    Items []string
}

type Consumer interface {
    Poll() Records
}

由于当前Pipeline的设计是通过plugin.Input接口来进行数据接收,因此kafka.Consumer并不能直接集成到系统中。

怎么办?使用适配器模式!

为了能让Pipeline能够使用kafka.Consumer接口,我们需要定义一个适配器如下:

package plugin
...
type KafkaInput struct {
    status Status
    consumer kafka.Consumer
}

func (k *KafkaInput) Receive() *msg.Message {
    records := k.consumer.Poll()
    if k.status != Started {
        fmt.Println("Kafka input plugin is not running, input nothing.")
        return nil
    }
    return msg.Builder().
        WithHeaderItem("content", "text").
        WithBodyItems(records.Items).
        Build()
}

// 在输入插件映射关系中加入kafka,用于通过反射创建input对象
func init() {
    inputNames["hello"] = reflect.TypeOf(HelloInput{})
    inputNames["kafka"] = reflect.TypeOf(KafkaInput{})
}
...

因为Go语言并没有构造函数,如果按照上一篇文章中的抽象工厂模式来创建KafkaInput,那么得到的实例中的consumer成员因为没有被初始化而会是nil。因此,需要给Plugin接口新增一个Init方法,用于定义插件的一些初始化操作,并在工厂返回实例前调用。

package plugin
...
type Plugin interface {
    Start()
    Stop()
    Status() Status
    // 新增初始化方法,在插件工厂返回实例前调用
    Init()
}

// 修改后的插件工厂实现如下
func (i *InputFactory) Create(conf Config) Plugin {
    t, _ := inputNames[conf.Name]
    p := reflect.New(t).Interface().(Plugin)
  // 返回插件实例前调用Init函数,完成相关初始化方法
    p.Init()
    return p
}

// KakkaInput的Init函数实现
func (k *KafkaInput) Init() {
    k.consumer = &kafka.MockConsumer{}
}

上述代码中的kafka.MockConsumer为我们模式Kafka消费者的一个实现,代码如下:

package kafka
...
type MockConsumer struct {}

func (m *MockConsumer) Poll() *Records {
    records := &Records{}
    records.Items = append(records.Items, "i am mock consumer.")
    return records
}

测试代码如下:

package test
...
func TestKafkaInputPipeline(t *testing.T) {
    config := pipeline.Config{
        Name: "pipeline2",
        Input: plugin.Config{
            PluginType: plugin.InputType,
            Name:       "kafka",
        },
        Filter: plugin.Config{
            PluginType: plugin.FilterType,
            Name:       "upper",
        },
        Output: plugin.Config{
            PluginType: plugin.OutputType,
            Name:       "console",
        },
    }
    p := pipeline.Of(config)
    p.Start()
    p.Exec()
    p.Stop()
}
// 运行结果
=== RUN   TestKafkaInputPipeline
Console output plugin started.
Upper filter plugin started.
Kafka input plugin started.
Pipeline started.
Output:
    Header:map[content:kafka], Body:[I AM MOCK CONSUMER.]
Kafka input plugin stopped.
Upper filter plugin stopped.
Console output plugin stopped.
Pipeline stopped.
--- PASS: TestKafkaInputPipeline (0.00s)
PASS

 

三、桥接模式(Bridge Pattern)

Go语言设计模式之结构型模式

 

3.1、简述

桥接模式主要用于将抽象部分和实现部分进行解耦,使得它们能够各自往独立的方向变化。它解决了在模块有多种变化方向的情况下,用继承所导致的类爆炸问题。举一个例子,一个产品有形状和颜色两个特征(变化方向),其中形状分为方形和圆形,颜色分为红色和蓝色。如果采用继承的设计方案,那么就需要新增4个产品子类:方形红色、圆形红色、方形蓝色、圆形红色。如果形状总共有m种变化,颜色有n种变化,那么就需要新增m*n个产品子类!现在我们使用桥接模式进行优化,将形状和颜色分别设计为一个抽象接口独立出来,这样需要新增2个形状子类:方形和圆形,以及2个颜色子类:红色和蓝色。同样,如果形状总共有m种变化,颜色有n种变化,总共只需要新增m+n个子类!

Go语言设计模式之结构型模式

上述例子中,我们通过将形状和颜色抽象为一个接口,使产品不再依赖于具体的形状和颜色细节,从而达到了解耦的目的。桥接模式本质上就是面向接口编程,可以给系统带来很好的灵活性和可扩展性。如果一个对象存在多个变化的方向,而且每个变化方向都需要扩展,那么使用桥接模式进行设计那是再合适不过了。

 

3.2、Go实现

回到消息处理系统的例子,一个Pipeline对象主要由Input、Filter、Output三类插件组成(3个特征),因为是插件化的系统,不可避免的就要求支持多种Input、Filter、Output的实现,并能够灵活组合(有多个变化的方向)。显然,Pipeline就非常适合使用桥接模式进行设计,实际上我们也这么做了。我们将Input、Filter、Output分别设计成一个抽象的接口,它们按照各自的方向去扩展。Pipeline只依赖的这3个抽象接口,并不感知具体实现的细节。

Go语言设计模式之结构型模式

package plugin
...
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)
}
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)
}

测试代码如下:

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.
Pipeline stopped.
--- PASS: TestPipeline (0.00s)
PASS

 

四、总结

本文主要介绍了结构型模式中的组合模式、适配器模式和桥接模式。组合模式主要解决代码复用的问题,相比于继承关系,组合模式可以避免继承层次过深导致的代码复杂问题,因此面向对象设计领域流传着组合优于继承的原则,而Go语言的设计也很好实践了该原则;适配器模式可以看作是两个不兼容接口之间的桥梁,可以将一个接口转换成Client所希望的另外一个接口,解决了模块之间因为接口不兼容而无法一起工作的问题;桥接模式将模块的抽象部分和实现部分进行分离,让它们能够往各自的方向扩展,从而达到解耦的目的。

以上就是Go语言设计模式之结构型模式的详细内容,更多关于Go结构型模式的资料请关注三水点靠木其它相关文章!

Golang 相关文章推荐
goland设置颜色和字体的操作
May 05 Golang
浅谈golang package中init方法的多处定义及运行顺序问题
May 06 Golang
Golang 实现获取当前函数名称和文件行号等操作
May 08 Golang
详解Go语言Slice作为函数参数的使用
Jul 02 Golang
K8s部署发布Golang应用程序的实现方法
Jul 16 Golang
go goroutine 怎样进行错误处理
Jul 16 Golang
深入理解go slice结构
Sep 15 Golang
如何解决goland,idea全局搜索快捷键失效问题
Apr 03 Golang
Golang日志包的使用
Apr 20 Golang
GoFrame框架数据校验之校验结果Error接口对象
Jun 21 Golang
Go语言怎么使用变长参数函数
Jul 15 Golang
Go语言测试库testify使用学习
Jul 23 Golang
浅谈Go语言多态的实现与interface使用
Jun 16 #Golang
再次探讨go实现无限 buffer 的 channel方法
Jun 13 #Golang
Go遍历struct,map,slice的实现
Jun 13 #Golang
go web 预防跨站脚本的实现方式
Jun 11 #Golang
Golang生成Excel文档的方法步骤
Go timer如何调度
浅谈Golang 切片(slice)扩容机制的原理
Jun 09 #Golang
You might like
header()函数使用说明
2006/11/23 PHP
php 调试利器debug_print_backtrace()
2012/07/23 PHP
DOM XPATH获取img src值的query
2013/09/23 PHP
PHP的Yii框架中View视图的使用进阶
2016/03/29 PHP
解决PHP curl或file_get_contents下载图片损坏或无法打开的问题
2019/10/11 PHP
php设计模式之代理模式分析【星际争霸游戏案例】
2020/03/23 PHP
PHP7 弃用功能
2021/03/09 PHP
Jquery+asp.net后台数据传到前台js进行解析的方法
2014/05/11 Javascript
jfreechart插件将数据展示成饼状图、柱状图和折线图
2015/04/13 Javascript
Bootstrap Table的使用总结
2016/10/08 Javascript
微信开发 js实现tabs选项卡效果
2016/10/28 Javascript
JavaScript实现的浏览器下载文件的方法
2017/08/09 Javascript
webpack+vue中使用别名路径引用静态图片地址
2017/11/20 Javascript
Django中日期处理注意事项与自定义时间格式转换详解
2018/08/06 Python
详解Python发送email的三种方式
2018/10/18 Python
python urllib爬虫模块使用解析
2019/09/05 Python
Python 实现Numpy中找出array中最大值所对应的行和列
2019/11/26 Python
Python 内置变量和函数的查看及说明介绍
2019/12/25 Python
Pytorch的mean和std调查实例
2020/01/02 Python
python 使用多线程创建一个Buffer缓存器的实现思路
2020/07/02 Python
5分钟快速掌握Python定时任务框架的实现
2021/01/26 Python
css3media响应式布局实例
2016/07/08 HTML / CSS
HTML5中canvas中的beginPath()和closePath()的重要性
2018/08/24 HTML / CSS
英国性感内衣和睡衣品牌:Bluebella
2018/01/26 全球购物
英国屋顶用品和材料超市:Roofing Supplies UK
2019/08/24 全球购物
车贷收入证明范本
2014/01/09 职场文书
大学毕业感言100字
2014/02/03 职场文书
倡议书范文
2014/04/16 职场文书
设备售后服务承诺书
2014/05/30 职场文书
爱的奉献演讲稿
2014/09/10 职场文书
慰问信的写作格式及范文!
2019/06/24 职场文书
python opencv通过按键采集图片源码
2021/05/20 Python
MySQL开启事务的方式
2021/06/26 MySQL
python ConfigParser库的使用及遇到的坑
2022/02/12 Python
Redis全局ID生成器的实现
2022/06/05 Redis
html中两种获取标签内的值的方法
2022/06/10 HTML / CSS