【Go实现】实践GoF的23种设计模式:桥接模式

2024-04-13 13:36

本文主要是介绍【Go实现】实践GoF的23种设计模式:桥接模式,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

上一篇:【Go实现】实践GoF的23种设计模式:解释器模式

简单的分布式应用系统(示例代码工程):https://github.com/ruanrunxue/Practice-Design-Pattern–Go-Implementation

简介

GoF 对桥接模式(Bridge Pattern)的定义如下:

Decouple an abstraction from its implementation so that the two can vary independently.

也即,将抽象部分和实现部分进行解耦,使得它们能够各自往独立的方向变化

桥接模式解决了在模块有多种变化方向的情况下,用继承所导致的类爆炸问题。

举个例子,一个产品有形状和颜色两个特征(变化方向),其中形状分为方形和圆形,颜色分为红色和蓝色。如果采用继承的设计方案,那么就需要新增4个产品子类:方形红色、圆形红色、方形蓝色、圆形红色。如果形状总共有 m 种变化,颜色有 n 种变化,那么就需要新增 m * n 个产品子类!

现在我们使用桥接模式进行优化,将形状和颜色分别设计为抽象接口独立出来,这样需要新增 2 个形状子类:方形和圆形,以及 2 个颜色子类:红色和蓝色。同样,如果形状总共有 m 种变化,颜色有 n 种变化,总共只需要新增 m + n 个子类!

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

当然,Go 语言从语言特性本身就把继承剔除,但桥接模式中分离变化、面向接口编程的思想仍然值得学习。

UML 结构

场景上下文

在 简单的分布式应用系统(示例代码工程)中,我们设计了一个 Monitor 监控系统模块,它可以看成是一个简单的 ETL 系统,负责对监控数据进行采集、处理、输出。监控数据来源于在线商场服务集群各个服务,当前通过消息队列模块 Mq 传递到监控系统,经处理后,存储到数据库模块 Db 上。

我们假设未来要上线一个不支持对接消息队列的服务、结果数据也需要存储到 ClickHouse 以供后续分析,为了应对未来多变的需求,我们有必要将监控系统设计得足够的可扩展。

于是,整个模块被设计为插件化风格的架构,Pipeline 是数据处理的流水线,其中包含了 InputFilterOutput 三类插件,Input 负责从各类数据源中获取监控数据,Filter 负责数据处理,Output 负责将处理后的数据输出。

上述设计中,我们抽象出 InputFilterOutput 三类插件,它们各种往独立的方向变化,最后在 Pipeline 上进行灵活组合,这使用桥接模式正合适。

代码实现

// 关键点1:明确产品的变化点,这里是input、filter和output三类插件,它们各自变化// demo/monitor/input/input_plugin.go
package input// 关键点2:将产品的变化点抽象成接口,这里是input.Plugin,filter.Plugin和output.Plugin
// Plugin 输入插件
type Plugin interface {plugin.PluginInput() (*plugin.Event, error)
}// 关键点3:实现产品变化点的接口,这里是SocketInput, AddTimestampFilter和MemoryDbOutput
// demo/monitor/input/socket_input.go
type SocketInput struct {socket      network.Socketendpoint    network.Endpointpackets     chan *network.PacketisUninstall uint32
}func (s *SocketInput) Input() (*plugin.Event, error) {packet, ok := <-s.packetsif !ok {return nil, plugin.ErrPluginUninstalled}event := plugin.NewEvent(packet.Payload())event.AddHeader("peer", packet.Src().String())return event, nil
}// demo/monitor/filter/filter_plugin.go
package filter// Plugin 过滤插件
type Plugin interface {plugin.PluginFilter(event *plugin.Event) *plugin.Event
}// demo/monitor/filter/add_timestamp_filter.go
// AddTimestampFilter 为MonitorRecord增加时间戳
type AddTimestampFilter struct {
}func (a *AddTimestampFilter) Filter(event *plugin.Event) *plugin.Event {re, ok := event.Payload().(*model.MonitorRecord)if !ok {return event}re.Timestamp = time.Now().Unix()return plugin.NewEvent(re)
}// demo/monitor/output/output_plugin.go
// Plugin 输出插件
type Plugin interface {plugin.PluginOutput(event *plugin.Event) error
}// demo/monitor/output/memory_db_output.go
type MemoryDbOutput struct {db        db.DbtableName string
}func (m *MemoryDbOutput) Output(event *plugin.Event) error {r, ok := event.Payload().(*model.MonitorRecord)if !ok {return fmt.Errorf("memory db output unknown event type %T", event.Payload())}return m.db.Insert(m.tableName, r.Id, r)
}// 关键点4:定义产品的接口或者实现,通过组合的方式把变化点桥接起来。
// demo/monitor/pipeline/pipeline_plugin.go
// Plugin pipeline由input、filter、output三种插件组成,定义了一个数据处理流程
// 数据流向为 input -> filter -> output
// 如果是接口,可以通过定义Setter方法达到聚合的目的。
type Plugin interface {plugin.PluginSetInput(input input.Plugin)SetFilter(filter filter.Plugin)SetOutput(output output.Plugin)
}// 如果是结构体,直接把变化点作为成员变量来达到聚合的目的。
type pipelineTemplate struct {input   input.Pluginfilter  filter.Pluginoutput  output.PluginisClose uint32run     func()
}func (p *pipelineTemplate) SetInput(input input.Plugin) {p.input = input
}func (p *pipelineTemplate) SetFilter(filter filter.Plugin) {p.filter = filter
}func (p *pipelineTemplate) SetOutput(output output.Plugin) {p.output = output
}// demo/monitor/pipeline/simple_pipeline.go
// SimplePipeline 简单Pipeline实现,每次运行时新启一个goroutine
type SimplePipeline struct {pipelineTemplate
}

在本系统中,我们通过配置文件来灵活组合插件,利用反射来实现插件的实例化,实例化的实现使用了抽象工厂模式,详细的实现方法可参考【Go实现】实践GoF的23种设计模式:抽象工厂模式。

总结实现桥接模式的几个关键点:

  1. 明确产品的变化点,这里是 input、filter 和 output 三类插件,它们各自变化。
  2. 将产品的变化点抽象成接口,这里是 input.Pluginfilter.Pluginoutput.Plugin
  3. 实现产品变化点的接口,这里是 SocketInput, AddTimestampFilterMemoryDbOutput
  4. 定义产品的接口或者实现,通过组合的方式把变化点桥接起来。这里是 pipeline.Plugin 通过 Setter 方法将input.Pluginfilter.Pluginoutput.Plugin 三个抽象接口桥接了起来。后面即可实现各类 input、filter 和 output 的灵活组合了。

扩展

TiDB 中的桥接模式

TiDB 是一款出色的分布式关系型数据库,它对外提供了一套插件框架,方便用户进行功能扩展。TiDB 的插件框架的设计,也运用到了桥接模式的思想。

如上图所示,每个 Plugin 都包含 ValidateOnInitOnShutdownOnFlush 四个待用户实现的接口,它们可以按照各自的方向去变化,然后灵活组合在 Plugin 中。

// Plugin presents a TiDB plugin.
type Plugin struct {*Manifestlibrary  *gplugin.PluginPath     stringDisabled uint32State    State
}// Manifest describes plugin info and how it can do by plugin itself.
type Manifest struct {Name           stringDescription    stringRequireVersion map[string]uint16License        stringBuildTime      string// Validate defines the validate logic for plugin.// returns error will stop load plugin process and TiDB startup.Validate func(ctx context.Context, manifest *Manifest) error// OnInit defines the plugin init logic.// it will be called after domain init.// return error will stop load plugin process and TiDB startup.OnInit func(ctx context.Context, manifest *Manifest) error// OnShutDown defines the plugin cleanup logic.// return error will write log and continue shutdown.OnShutdown func(ctx context.Context, manifest *Manifest) error// OnFlush defines flush logic after executed `flush tidb plugins`.// it will be called after OnInit.// return error will write log and continue watch following flush.OnFlush      func(ctx context.Context, manifest *Manifest) errorflushWatcher *flushWatcherVersion uint16Kind    Kind
}

TiDB 在实现插件框架时,使用函数式编程的方式来定义 OnXXX 接口,更具有 Go 风格。

典型应用场景

  • 从多个维度上对系统/类/结构体进行扩展,如插件化架构。
  • 在运行时切换不同的实现,如插件化架构。
  • 用于构建与平台无关的程序适配层。

优缺点

优点

  • 可实现抽象不分与实现解耦,变化实现时,客户端无须修改代码,符合开闭原则。
  • 每个分离的变化点都可以专注于自身的演进,符合单一职责原则。

缺点

  • 过度的抽象(过度设计)会使得接口膨胀,导致系统复杂性变大,难以维护。

与其他模式的关联

桥接模式通常与抽象工厂模式搭配使用,比如,在本文例子中,可以通过抽象工厂模式对各个 Plugin 完成实例化,详情见【Go实现】实践GoF的23种设计模式:抽象工厂模式。

文章配图

可以在 用Keynote画出手绘风格的配图 中找到文章的绘图方法。

参考

[1] 【Go实现】实践GoF的23种设计模式:SOLID原则, 元闰子

[2] Design Patterns, Chapter 4. Structural Patterns, GoF

[3] 【Go实现】实践GoF的23种设计模式:抽象工厂模式, 元闰子

[4] 桥接模式, refactoringguru.cn

更多文章请关注微信公众号:元闰子的邀请

这篇关于【Go实现】实践GoF的23种设计模式:桥接模式的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/900284

相关文章

C++使用栈实现括号匹配的代码详解

《C++使用栈实现括号匹配的代码详解》在编程中,括号匹配是一个常见问题,尤其是在处理数学表达式、编译器解析等任务时,栈是一种非常适合处理此类问题的数据结构,能够精确地管理括号的匹配问题,本文将通过C+... 目录引言问题描述代码讲解代码解析栈的状态表示测试总结引言在编程中,括号匹配是一个常见问题,尤其是在

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

如何通过Python实现一个消息队列

《如何通过Python实现一个消息队列》这篇文章主要为大家详细介绍了如何通过Python实现一个简单的消息队列,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录如何通过 python 实现消息队列如何把 http 请求放在队列中执行1. 使用 queue.Queue 和 reque

Python如何实现PDF隐私信息检测

《Python如何实现PDF隐私信息检测》随着越来越多的个人信息以电子形式存储和传输,确保这些信息的安全至关重要,本文将介绍如何使用Python检测PDF文件中的隐私信息,需要的可以参考下... 目录项目背景技术栈代码解析功能说明运行结php果在当今,数据隐私保护变得尤为重要。随着越来越多的个人信息以电子形

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景