摘要:本文主要聚焦在结构型模式(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则为消息的组成部分。

  1. type Message struct {
  2. Header *Header
  3. Body *Body
  4. }

现在,我们来看一个稍微复杂一点的例子,同样考虑上一篇文章中所描述的插件架构风格的消息处理系统。前面我们用抽象工厂模式解决了插件加载的问题,通常,每个插件都会有一个生命周期,常见的就是启动状态和停止状态,现在我们使用组合模式来解决插件的启动和停止问题。

首先给Plugin接口添加几个生命周期相关的方法:

  1. package plugin
  2. ...
  3. // 插件运行状态
  4. type Status uint8
  5.  
  6. const (
  7. Stopped Status = iota
  8. Started
  9. )
  10.  
  11. type Plugin interface {
  12. // 启动插件
  13. Start()
  14. // 停止插件
  15. Stop()
  16. // 返回插件当前的运行状态
  17. Status() Status
  18. }
  19. // Input、Filter、Output三类插件接口的定义跟上一篇文章类似
  20. // 这里使用Message结构体替代了原来的string,使得语义更清晰
  21. type Input interface {
  22. Plugin
  23. Receive() *msg.Message
  24. }
  25.  
  26. type Filter interface {
  27. Plugin
  28. Process(msg *msg.Message) *msg.Message
  29. }
  30.  
  31. type Output interface {
  32. Plugin
  33. Send(msg *msg.Message)
  34. }

对于插件化的消息处理系统而言,一切皆是插件,因此我们将Pipeine也设计成一个插件,实现Plugin接口:

  1. package pipeline
  2. ...
  3. // 一个Pipeline由input、filter、output三个Plugin组成
  4. type Pipeline struct {
  5. status plugin.Status
  6. input plugin.Input
  7. filter plugin.Filter
  8. output plugin.Output
  9. }
  10.  
  11. func (p *Pipeline) Exec() {
  12. msg := p.input.Receive()
  13. msg = p.filter.Process(msg)
  14. p.output.Send(msg)
  15. }
  16. // 启动的顺序 output -> filter -> input
  17. func (p *Pipeline) Start() {
  18. p.output.Start()
  19. p.filter.Start()
  20. p.input.Start()
  21. p.status = plugin.Started
  22. fmt.Println("Hello input plugin started.")
  23. }
  24. // 停止的顺序 input -> filter -> output
  25. func (p *Pipeline) Stop() {
  26. p.input.Stop()
  27. p.filter.Stop()
  28. p.output.Stop()
  29. p.status = plugin.Stopped
  30. fmt.Println("Hello input plugin stopped.")
  31. }
  32.  
  33. func (p *Pipeline) Status() plugin.Status {
  34. return p.status
  35. }

一个Pipeline由Input、Filter、Output三类插件组成,形成了“部分-整体”的关系,而且它们都实现了Plugin接口,这就是一个典型的组合模式的实现。Client无需显式地启动和停止Input、Filter和Output插件,在调用Pipeline对象的Start和Stop方法时,Pipeline就已经帮你按顺序完成对应插件的启动和停止。

相比于上一篇文章,在本文中实现Input、Filter、Output三类插件时,需要多实现3个生命周期的方法。还是以上一篇文章中的HelloInput、UpperFilter和ConsoleOutput作为例子,具体实现如下:

  1. package plugin
  2. ...
  3. type HelloInput struct {
  4. status Status
  5. }
  6.  
  7. func (h *HelloInput) Receive() *msg.Message {
  8. // 如果插件未启动,则返回nil
  9. if h.status != Started {
  10. fmt.Println("Hello input plugin is not running, input nothing.")
  11. return nil
  12. }
  13. return msg.Builder().
  14. WithHeaderItem("content", "text").
  15. WithBodyItem("Hello World").
  16. Build()
  17. }
  18.  
  19. func (h *HelloInput) Start() {
  20. h.status = Started
  21. fmt.Println("Hello input plugin started.")
  22. }
  23.  
  24. func (h *HelloInput) Stop() {
  25. h.status = Stopped
  26. fmt.Println("Hello input plugin stopped.")
  27. }
  28.  
  29. func (h *HelloInput) Status() Status {
  30. return h.status
  31. }
  32. package plugin
  33. ...
  34. type UpperFilter struct {
  35. status Status
  36. }
  37.  
  38. func (u *UpperFilter) Process(msg *msg.Message) *msg.Message {
  39. if u.status != Started {
  40. fmt.Println("Upper filter plugin is not running, filter nothing.")
  41. return msg
  42. }
  43. for i, val := range msg.Body.Items {
  44. msg.Body.Items[i] = strings.ToUpper(val)
  45. }
  46. return msg
  47. }
  48.  
  49. func (u *UpperFilter) Start() {
  50. u.status = Started
  51. fmt.Println("Upper filter plugin started.")
  52. }
  53.  
  54. func (u *UpperFilter) Stop() {
  55. u.status = Stopped
  56. fmt.Println("Upper filter plugin stopped.")
  57. }
  58.  
  59. func (u *UpperFilter) Status() Status {
  60. return u.status
  61. }
  62.  
  63. package plugin
  64. ...
  65. type ConsoleOutput struct {
  66. status Status
  67. }
  68.  
  69. func (c *ConsoleOutput) Send(msg *msg.Message) {
  70. if c.status != Started {
  71. fmt.Println("Console output is not running, output nothing.")
  72. return
  73. }
  74. fmt.Printf("Output:\n\tHeader:%+v, Body:%+v\n", msg.Header.Items, msg.Body.Items)
  75. }
  76.  
  77. func (c *ConsoleOutput) Start() {
  78. c.status = Started
  79. fmt.Println("Console output plugin started.")
  80. }
  81.  
  82. func (c *ConsoleOutput) Stop() {
  83. c.status = Stopped
  84. fmt.Println("Console output plugin stopped.")
  85. }
  86.  
  87. func (c *ConsoleOutput) Status() Status {
  88. return c.status
  89. }

测试代码如下:

  1. package test
  2. ...
  3. func TestPipeline(t *testing.T) {
  4. p := pipeline.Of(pipeline.DefaultConfig())
  5. p.Start()
  6. p.Exec()
  7. p.Stop()
  8. }
  9. // 运行结果
  10. === RUN TestPipeline
  11. Console output plugin started.
  12. Upper filter plugin started.
  13. Hello input plugin started.
  14. Pipeline started.
  15. Output:
  16. Header:map[content:text], Body:[HELLO WORLD]
  17. Hello input plugin stopped.
  18. Upper filter plugin stopped.
  19. Console output plugin stopped.
  20. Hello input plugin stopped.
  21. --- PASS: TestPipeline (0.00s)
  22. PASS

组合模式的另一种实现,嵌入组合(Embedding Composition),其实就是利用了Go语言的匿名成员特性,本质上跟直接组合是一致的。

还是以Message结构体为例,如果采用嵌入组合,则看起来像是这样:

  1. type Message struct {
  2. Header
  3. Body
  4. }
  5. // 使用时,Message可以引用Header和Body的成员属性,例如:
  6. msg := &Message{}
  7. msg.SrcAddr = "192.168.0.1"

适配器模式(Adapter Pattern)

简述

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

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

Go实现

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

  1. package kafka
  2. ...
  3. type Records struct {
  4. Items []string
  5. }
  6.  
  7. type Consumer interface {
  8. Poll() Records
  9. }

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

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

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

  1. package plugin
  2. ...
  3. type KafkaInput struct {
  4. status Status
  5. consumer kafka.Consumer
  6. }
  7.  
  8. func (k *KafkaInput) Receive() *msg.Message {
  9. records := k.consumer.Poll()
  10. if k.status != Started {
  11. fmt.Println("Kafka input plugin is not running, input nothing.")
  12. return nil
  13. }
  14. return msg.Builder().
  15. WithHeaderItem("content", "text").
  16. WithBodyItems(records.Items).
  17. Build()
  18. }
  19.  
  20. // 在输入插件映射关系中加入kafka,用于通过反射创建input对象
  21. func init() {
  22. inputNames["hello"] = reflect.TypeOf(HelloInput{})
  23. inputNames["kafka"] = reflect.TypeOf(KafkaInput{})
  24. }
  25. ...

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

  1. package plugin
  2. ...
  3. type Plugin interface {
  4. Start()
  5. Stop()
  6. Status() Status
  7. // 新增初始化方法,在插件工厂返回实例前调用
  8. Init()
  9. }
  10.  
  11. // 修改后的插件工厂实现如下
  12. func (i *InputFactory) Create(conf Config) Plugin {
  13. t, _ := inputNames[conf.Name]
  14. p := reflect.New(t).Interface().(Plugin)
  15. // 返回插件实例前调用Init函数,完成相关初始化方法
  16. p.Init()
  17. return p
  18. }
  19.  
  20. // KakkaInput的Init函数实现
  21. func (k *KafkaInput) Init() {
  22. k.consumer = &kafka.MockConsumer{}
  23. }

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

  1. package kafka
  2. ...
  3. type MockConsumer struct {}
  4.  
  5. func (m *MockConsumer) Poll() *Records {
  6. records := &Records{}
  7. records.Items = append(records.Items, "i am mock consumer.")
  8. return records
  9. }

测试代码如下:

  1. package test
  2. ...
  3. func TestKafkaInputPipeline(t *testing.T) {
  4. config := pipeline.Config{
  5. Name: "pipeline2",
  6. Input: plugin.Config{
  7. PluginType: plugin.InputType,
  8. Name: "kafka",
  9. },
  10. Filter: plugin.Config{
  11. PluginType: plugin.FilterType,
  12. Name: "upper",
  13. },
  14. Output: plugin.Config{
  15. PluginType: plugin.OutputType,
  16. Name: "console",
  17. },
  18. }
  19. p := pipeline.Of(config)
  20. p.Start()
  21. p.Exec()
  22. p.Stop()
  23. }
  24. // 运行结果
  25. === RUN TestKafkaInputPipeline
  26. Console output plugin started.
  27. Upper filter plugin started.
  28. Kafka input plugin started.
  29. Pipeline started.
  30. Output:
  31. Header:map[content:kafka], Body:[I AM MOCK CONSUMER.]
  32. Kafka input plugin stopped.
  33. Upper filter plugin stopped.
  34. Console output plugin stopped.
  35. Pipeline stopped.
  36. --- PASS: TestKafkaInputPipeline (0.00s)
  37. 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个抽象接口,并不感知具体实现的细节。

  1. package plugin
  2. ...
  3. type Input interface {
  4. Plugin
  5. Receive() *msg.Message
  6. }
  7.  
  8. type Filter interface {
  9. Plugin
  10. Process(msg *msg.Message) *msg.Message
  11. }
  12.  
  13. type Output interface {
  14. Plugin
  15. Send(msg *msg.Message)
  16. }
  17. package pipeline
  18. ...
  19. // 一个Pipeline由input、filter、output三个Plugin组成
  20. type Pipeline struct {
  21. status plugin.Status
  22. input plugin.Input
  23. filter plugin.Filter
  24. output plugin.Output
  25. }
  26. // 通过抽象接口来使用,看不到底层的实现细节
  27. func (p *Pipeline) Exec() {
  28. msg := p.input.Receive()
  29. msg = p.filter.Process(msg)
  30. p.output.Send(msg)
  31. }

测试代码如下:

  1. package test
  2. ...
  3. func TestPipeline(t *testing.T) {
  4. p := pipeline.Of(pipeline.DefaultConfig())
  5. p.Start()
  6. p.Exec()
  7. p.Stop()
  8. }
  9. // 运行结果
  10. === RUN TestPipeline
  11. Console output plugin started.
  12. Upper filter plugin started.
  13. Hello input plugin started.
  14. Pipeline started.
  15. Output:
  16. Header:map[content:text], Body:[HELLO WORLD]
  17. Hello input plugin stopped.
  18. Upper filter plugin stopped.
  19. Console output plugin stopped.
  20. Pipeline stopped.
  21. --- PASS: TestPipeline (0.00s)
  22. 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. RxJava线程控制

    RxJava中的线程转换主要通过下面两个方法: 1.subscribeOn 2.observeOn 一.subscribeOn 1.调用一次subscribeOn时: Observable obser ...

  2. Error starting userland proxy: /forwards/expose/port returned unexpected status: 500.

    欢迎关注微信公众号 Error starting userland proxy: /forwards/expose/port returned unexpected status: 500. dock ...

  3. liunx中文件夹不能删除怎么操作

    1.运行rm -rf 文件名称 2.不能删除对应文件并且提示"rm: cannot remove './.user.ini': Operation not permitted" 操 ...

  4. 反向解析 参数替换 reverse

  5. 佳能m62套机5500 佳能EOS M50 M6 MARK2 II二代 最低到过5800

    佳能m62套机5500 佳能EOS M50 M6 MARK2 II二代

  6. SpringBoot2 单元测试类的报错问题

    问题描述 执行 SpringBoot2 测试时报错,提示找不到 SsmApplicationTests 主类 原因分析 Junit5 升级了框架没有兼容 问题解决 <!--测试模块--> ...

  7. STM32的时钟系统RCC详细整理(转载)

    一.综述: 1.时钟源 在 STM32 中,一共有 5 个时钟源,分别是 HSI . HSE . LSI . LSE . PLL . ①HSI 是高速内部时钟, RC 振荡器,频率为 8MHz : ② ...

  8. C++ short/int/long/long long 等数据类型大小

    表 1 整型数据类型 数据类型 字节大小 数值范围 short int (短整型) 2 字节 -32 768 〜+32 767 unsigned short int(无符号短整型) 2 字节 0 〜+ ...

  9. Bootstrap Bootstrap3 与 Bootstrap4 的区别

    Bootstrap3 与 Bootstrap4 官网地址 Bootstrap3 官网:https://v3.bootcss.com Bootstrap4 官网:https://v4.bootcss.c ...

  10. 使用 .NET 升级助手将.NET Framework应用迁移到.NET 5

    从.NET Framework 迁移到.NET 5 犹如搬家,我们都知道搬家是很痛苦的,我们请求搬家公司来减轻我们的压力,.NET 升级助手 的作用就类似我们聘请的搬家公司,帮助我们处理繁重乏味的迁移 ...