【Go实现】实践GoF的23种设计模式:SOLID原则( 四 )


我们可以发现,上述方案的演进过程,就是我们不断对业务依赖的数据库模块进行抽象的过程,最终设计出稳定的、服务OCP的软件 。
那么,在编程语言中,我们用什么来表示“数据库”这一抽象呢?是接口!
数据库最常见的几个操作就是CRUD,因此我们可以设计这么一个Db接口来表示“数据库”:
type Db interface {Query(tableName string, cond Condition) (*Record, error)Insert(tableName string, record *Record) errorUpdate(tableName string, record *Record) errorDelete(tableName string, record *Record) error}
这样,业务模块和数据库模块之间的依赖关系就变成如下图所示:
满足OCP的另一个关键点就是分离变化,只有先把变化点识别分离出来,我们才能对它进行抽象化 。下面以我们的分布式应用系统demo为例,解释如何实现变化点的分离和抽象 。
在demo中,监控系统主要负责对服务的 log进行ETL操作,也即涉及如下3个操作:1)从消息队列中获取日志数据;2)对数据进行加工;3)将加工后的数据存储在数据库上 。
我们把整一个日志数据的处理流程称为,那么我们可以这么实现:
type Pipeline struct {mq Mqdb Db ...}func (p *Pipeline) Run() {for atomic.LoadUint32(&p.isClose) != 1 {// 1、从消息队列中获取数据msg := p.mq.Consume("monitor.topic")log := msg.Payload()// 2、对数据进行字段提取操作matches := p.pattern.FindStringSubmatch(log)if len(matches) != 3 {return event}record := model.NewMonitoryRecord()record.Endpoint = matches[1]record.Type = model.Type(matches[2])// 3.存储到数据库上p.db.Insert("logs_table", record.Id, record)...}}
现在考虑新上线一个服务,但是这个服务不支持对接消息队列了,只支持传输数据,于是我们得在上新增一个来判断是否使用输入源:
func (p *Pipeline) Run() {for atomic.LoadUint32(&p.isClose) != 1 {if inputType == input.MqType {// 从消息队列中获取数据msg := p.mq.Consume("monitor.topic")log := msg.Payload()} else {// 使用socket为消息来源packet := socket.Receice()log := packet.PayLoad().(string)}...}}
过一段时间,有需求需要给 log打上一个时间戳,方便后续的日志分析,于是我们需要修改的数据加工逻辑:
func (p *Pipeline) Run() {for atomic.LoadUint32(&p.isClose) != 1 {...// 对数据进行字段提取操作matches := p.pattern.FindStringSubmatch(log)if len(matches) != 3 {return event}record := model.NewMonitoryRecord()record.Endpoint = matches[1]record.Type = model.Type(matches[2])// 新增一个时间戳字段record.Timestamp = time.Now().Unix()...}}
很快,又有一个需求,需要将加工后的数据存储到ES上,方便后续的日志检索,于是我们再次修改了的数据存储逻辑:
func (p *Pipeline) Run() {for atomic.LoadUint32(&p.isClose) != 1 {...if outputType == output.Db {// 存储到ES上p.db.Insert("logs_table", record.Id, record)} else {// 存储到ES上p.es.Store(record.Id, record)}...}}
在上述的例子中,每次新增需求都需要修改模块,明显违反了OCP 。下面,我们来对它进行优化,使它满足OCP 。
第一步是分离变化点,根据的业务处理逻辑,我们可以发现3个独立的变化点,数据的获取、加工和存储 。第二步,我们对这3个变化点进行抽象,设计出以下3个抽象接口:
// demo/monitor/input/input_plugin.go// 数据获取抽象接口type InputPlugin interface {plugin.PluginInput() (*plugin.Event, error)}// demo/monitor/filter/filter_plugin.go// 数据加工抽象接口type FilterPlugin interface {plugin.PluginFilter(event *plugin.Event) *plugin.Event}// demo/monitor/output/output_plugin.go// 数据存储抽象接口type OutputPlugin interface {plugin.PluginOutput(event *plugin.Event) error}