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 相关文章推荐
golang判断key是否在map中的代码
Apr 24 Golang
golang interface判断为空nil的实现代码
Apr 24 Golang
Go语言使用select{}阻塞main函数介绍
Apr 25 Golang
Go使用协程交替打印字符
Apr 29 Golang
golang 比较浮点数的大小方式
May 02 Golang
go类型转换及与C的类型转换方式
May 05 Golang
golang 实现时间戳和时间的转化
May 07 Golang
Golang中异常处理机制详解
Jun 08 Golang
Go语言空白表示符_的实例用法
Jul 04 Golang
Golang MatrixOne使用介绍和汇编语法
Apr 19 Golang
Golang ort 中的sortInts 方法
Apr 24 Golang
GoFrame gredis缓存DoVar Conn连接对象 自动序列化GoFrame gredisDo/DoVar方法Conn连接对象自动序列化/反序列化总结
Jun 14 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
解析Linux下Varnish缓存的配置优化
2013/06/20 PHP
PHP操作MySQL事务实例
2014/11/05 PHP
php中static和const关键字用法分析
2016/12/07 PHP
javascript之卸载鼠标事件的代码
2007/05/14 Javascript
用js实现in_array的方法
2013/11/05 Javascript
JavaScript的常见兼容问题及相关解决方法(chrome/IE/firefox)
2013/12/31 Javascript
Javascript前端UI框架Kit使用指南之Kitjs简介
2014/11/28 Javascript
浅谈javascript 函数内部属性
2015/01/21 Javascript
JS中的THIS和WINDOW.EVENT.SRCELEMENT详解
2015/05/25 Javascript
javascript处理a标签超链接默认事件的方法
2015/06/29 Javascript
IE8兼容Jquery.validate.js的问题
2016/12/01 Javascript
了解ESlint和其相关操作小结
2018/05/21 Javascript
详解如何模拟实现node中的Events模块(通俗易懂版)
2019/04/15 Javascript
[47:06]DOTA2上海特级锦标赛主赛事日 - 4 败者组第五轮 MVP.Phx VS EG第一局
2016/03/05 DOTA
[01:34]2016国际邀请赛中国区预选赛IG战队教练采访
2016/06/27 DOTA
python网络编程学习笔记(八):XML生成与解析(DOM、ElementTree)
2014/06/09 Python
Python datetime时间格式化去掉前导0
2014/07/31 Python
python抽象基类用法实例分析
2015/06/04 Python
Python-嵌套列表list的全面解析
2016/06/08 Python
TensorFlow实现MLP多层感知机模型
2018/03/09 Python
Python paramiko模块的使用示例
2018/04/11 Python
Python 学习教程之networkx
2019/04/15 Python
tensorflow从ckpt和从.pb文件读取变量的值方式
2020/05/26 Python
Python模块zipfile原理及使用方法详解
2020/08/04 Python
Django CBV模型源码运行流程详解
2020/08/17 Python
css3 pointer-events 介绍详解
2017/09/18 HTML / CSS
CSS3实现红包抖动效果
2020/12/23 HTML / CSS
AmazeUI 加载进度条的实现示例
2020/08/20 HTML / CSS
泰国办公用品购物网站:OfficeMate
2018/02/04 全球购物
十佳教师事迹材料
2014/01/11 职场文书
先进集体事迹材料范文
2014/12/25 职场文书
农民工工资承诺书大全
2015/05/04 职场文书
因工资原因离职的辞职信范文
2015/05/12 职场文书
2015年教务处干事工作总结
2015/07/22 职场文书
烈士陵园扫墓感想
2015/08/07 职场文书
Spring Boot项目传参校验的最佳实践指南
2022/04/05 Java/Android