摘要:本文主要聚焦在结构型模式(Structural Pattern)上,其主要思想是将多个对象组装成较大的结构,并同时保持结构的灵活和高效,从程序的结构上解决模块之间的耦合问题。

本文分享自华为云社区《快来,这里有23种设计模式的Go语言实现(二)》,原文作者:元闰子。

本文主要聚焦在结构型模式(Structural Pattern)上,其主要思想是将多个对象组装成较大的结构,并同时保持结构的灵活和高效,从程序的结构上解决模块之间的耦合问题。

组合模式(Composite Pattern)

简述

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

我们都知道,组合所表示的语义为“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"

适配器模式(Adapter Pattern)

简述

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

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

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)

简述

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

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

Go实现

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

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语言实现的23种设计模式之结构型模式的更多相关文章

  1. GoF的23种设计模式之结构型模式的特点和分类

    结构型模式描述如何将类或对象按某种布局组成更大的结构.它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象. 由于组合关系或聚合关系比继承关系耦合度低,满足 ...

  2. Java经典23种设计模式之结构型模式(一)

    结构型模式包含7种:适配器模式.桥接模式.组合模式.装饰模式.外观模式.享元模式.代理模式. 本文主要介绍适配器模式和桥接模式. 一.适配器模式(Adapter) 适配器模式事实上非常easy.就像手 ...

  3. GoF23种设计模式之结构型模式之代理模式

    一.概述 为其他对象提供一种代理以控制对这个对象的访问. 二.适用性 1.远程代理(RemoteProxy):为一个对象在不同的地址空间土工局部代表. 2.虚代理(VirtualProxy):根据需要 ...

  4. GoF23种设计模式之结构型模式之桥接模式

    一.概述         将类的抽象部分与实现分部分离开来,使它们都可以独立地变化. 二.适用性 1.你不希望在抽象和实现之间有一个固定的绑定关系的时候.例如:在程序运行时实现部分应可以被选择或切换. ...

  5. GoF23种设计模式之结构型模式之适配器模式

    一.概述         将一个类的接口转换成客户希望的另外一个接口.适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作. 二.适用性 1.你想使用一个已经存在的类,但是它的接口不符合 ...

  6. GoF23种设计模式之结构型模式之组合模式

    一.概述 将对象组合成树型结构以表示“部分--整体”的层次关系.组合模式使得用户对单个对象和组合对象的使用具有一致性. 二.适用性 1.你想表示对象的部分--整体层次结构的时候. 2.你希望用户忽略组 ...

  7. GoF23种设计模式之结构型模式之外观模式

    一.概述         为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用. 二.适用性 1.当你要为一个复杂子系统提供一个简单接口的时候.子系统 ...

  8. GoF23种设计模式之结构型模式之装饰模式

    一.概述 动态地给一个对象添加一些额外的职责.装饰模式比生成子类更为灵活. 二.适用性 1.在不影响其他对象的情况下,以动态.透明的方式给但个对象添加职责. 2.处理那些可以撤销的职责. 3.当不能采 ...

  9. GoF23种设计模式之结构型模式之享元模式

    一.概述  运用共享技术有效地支持大量细粒度的对象. 二.适用性 1.当一个应用程序使用了大量的对象的时候. 2.由于使用大量的独享而造成很大的存储开销的时候. 3.对象的大多数状态都可变为外部状态的 ...

随机推荐

  1. python中的的异步IO

    asyncio 是干什么的? 异步网络操作 并发 协程 python3.0 时代,标准库里的异步网络模块:select(非常底层) python3.0时代,第三方异步网络库:Tornado pytho ...

  2. ElasticSearch第三弹之存储原理

    我们上文中介绍的ES内部索引的写处理流程是在ES的内存中执行的,而数据被分配到特定的主.副分片上之后,最终是存储到磁盘上的,这样在断电的时候就不会丢失数据.具体的存储路径可在配置文件 ../confi ...

  3. Blazor实现未登录重定向到登录页的方法

    今天研究了一下blazor,发现他默认启动就是类似于后台管理系统的界面,看到这个页面我就想给他写个登录,有登录就涉及到未登录重定向的问题,但是我没有找到blazor全局路由的设置,知道的老哥可以告诉我 ...

  4. 匿名函数lambda / map()方法

    lambda一般配合其他方法使用,一般使用在只用过一次就不用的函数,那就没必要特意去定义 lambda能支持的最复杂的语句就是三元运算 例如: lambda x,y: x*y if x < y ...

  5. docker中运行envoy 报错 cannot bind '0.0.0.0:80': Permission denied

    docker-compose文件 version: '3' services: envoy: image: envoyproxy/envoy-alpine:v1.15-latest volumes: ...

  6. [bug] HDFS:DataXceiver error processing WRITE_BLOCK operation

    文件格式有误,导致读取错误,我的是把制表符敲成了空格

  7. Java 单例模式:懒加载(延迟加载)和即时加载

    引言 在开发中,如果某个实例的创建需要消耗很多系统资源,那么我们通常会使用惰性加载机制(或懒加载.延时加载),也就是说只有当使用到这个实例的时候才会创建这个实例,这个好处在单例模式中得到了广泛应用.这 ...

  8. pip;python包管理工具

    刚开始学习Python时,在看文档和别人的blog介绍安装包有的用easy_install, setuptools, 有的使用pip,distribute,那麽这几个工具有什么关系呢,看一下下面这个图 ...

  9. python3 smtplib发送邮件

    使用smtp包发送邮件还依赖email的一些方法 发送邮件主要分为三步: 1,定义邮箱参数:邮箱服务器地址,邮箱用户名,邮箱密码,邮件发送方,邮件接收方,邮件标题,邮件内容 2,配置发送内容 3,实例 ...

  10. SUSE12 网卡配置、SSH远程配置、解决CRT密钥交换失败,没有兼容的加密程序

    安装好SUSE系统后发现网卡配置与Centos有些差异,多网卡的同学可以参考一下(我的是双网卡) SUSE系统默认第一块网卡自动获取IP,如果是多网卡,需要手动配置,由于我的第一个网卡获取正确无需更改 ...