关于结构体:Go语言实现的23种设计模式之结构型模式

36次阅读

共计 8711 个字符,预计需要花费 22 分钟才能阅读完成。

摘要:本文次要聚焦在结构型模式(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 所心愿的另外一个接口,解决了模块之间因为接口不兼容而无奈一起工作的问题;桥接模式将模块的形象局部和实现局部进行拆散,让它们可能往各自的方向扩大,从而达到解耦的目标。

点击关注,第一工夫理解华为云陈腐技术~

正文完
 0