go语言在solid原则中的优势

[读书笔记]SOLID原则

SOLID原则是用来做什么的?

solid原则主要是告诉我们如何把数据和函数组织成为类,以及如何把这些类连接起来成为程序。

PS.这里的类不一定是面向对象的,只是表示数据和函数的一种分组。

solid原则核心是描述软件的中层结构目标,致力于实现:

  1. 是软件更容易被扩展。
  2. 使软件更容易被理解。
  3. 使软件更容易被复用。
PS.还流传这一些其他版本的SOLID原则,比如加入了迪米特法则。

SOLID原则指的是什么?

SOLID原则是指五个设计原则,每个设计原则的首字母拼起来,刚好是SOLID这个单词。

这些设计原则主要有。

  • SRP: 单一职责原则。每个软件模块有且只有一个需要被改变的理由。
  • OCP: 开闭原则。软件系统应该允许通过新增代码来修改原有系统行为,而不是通过修改现有代码。
  • LSP: 里氏替换原则。实现某些接口的组件,必须同时遵守同一个约定,以便让这些组件可以相互替换。
  • ISP: 接口隔离原则。只依赖自己需要的部分。
  • DIP: 依赖倒置原则。调用方不应该依赖于被调用方的实现,而应该依赖于接口。

原则实践

SRP:单一职责原则

每个软件模块有且只有一个需要被改变的理由。

指南:不要强行复用代码。

go语言在solid原则中的优势_第1张图片

这种逻辑在我们的代码里面应该很常见,就是说我们计算员工薪资需要用到员工的工作时长数据,另一方面,企业报表系统也需要一份企业员工的工作时长数据,这两份数据也许一开始的计算逻辑是相同的。但是这样的复用非常危险,因为背后这两个逻辑服务的对象是不同的,他们很有可能提出不同的计算方法,比如薪酬部门提出每天晚上10点以后的工作时间不需要计算在内。

因此,我们在面临这种需求时需要谨慎,考虑清楚。

这个逻辑变化发生的时候变化的原因(需求方)是否相同。

并不是说所有的复用都是不对的,是来自不同需求方的相同逻辑不应该被复用,好比某一个角度相同的两个物体其实不是等价的。

OCP: 开闭原则

软件系统应该允许通过新增代码来修改原有系统行为,而不是通过修改现有代码。

开闭原则是一个大的指导方针。不包含具体的实现指导,所以这里不单独讲了。

LSP: 里氏替换原则

实现某些接口的组件,必须同时遵守同一个约定,以便让这些组件可以相互替换。

指南:不要破坏接口语义约定

这个原则理解起来其实就是不要破坏约定,已经广泛应用在软件架构领域,不局限在单独的接口层面了。典型的例子是正方形/长方形问题。

go语言在solid原则中的优势_第2张图片

Rectangle r = ...

r.SetW(5)

r.SetH(2)

assert(r.area() == 10);

长方形继承自正方形会打破这个测试用例,但是正方形又“伪装”成长方形,这是不对的。正确的做法应该是:正方形可以通过组合而的方式把长方形适配成一个正方形,User也不应该类似使用长方形一样来使用正方形。

go语言在solid原则中的优势_第3张图片

  • ISP: 接口隔离原则。
只依赖自己需要的部分。

指南:尽量使用小接口

尽量使用小的接口,而不是大接口,在这方面go语言是很好的践行者,golang鼓励使用小的接口通过组合的方式来构建大的接口。我们可以看到官方包里面存在大量的小接口。

//database/sql/driver包

type ConnBeginTx interface {

BeginTx(ctx context.Context, opts TxOptions) (Tx, error)

}

type ConnPrepareContext interface {

PrepareContext(ctx context.Context, query string) (Stmt, error)

}

上面这两个接口都是数据库的Conn需要实现的接口(如果支持事物的话),但是官方定义采用了将接口拆分开来的策略。

下图是stdlib、k8s和docker里面的interface定义,画出了下面这幅接口个数与接口中method个数关系的折线图:

go语言在solid原则中的优势_第4张图片

小接口方法少,职责单一;易于实现和测试,通用性强(如:io.Reader和Writer),易于组合(如:io.Reader)。

不过要想在业务领域定义出合适的小接口,还是需要对问题域有着透彻的理解的。往往无法定义出小接口,都是由于对领域的理解还不到位,没法抽象到很高的程度所致。

延伸一下,我们业务系统架构的时候,Repository应该是一个大的接口么?

type Repository interface {

SaveOrder(order Order)

SearchOrder(title string) []Order

}

这种设计并不是一个好的设计,SaveOrder和SearchOrder很有可能用的是不同的存储方式(假如是mysql和ES)。这时候我们Repository的实现就需要同时依赖Mysql和ES组件。

go语言在solid原则中的优势_第5张图片

这里面其实违反了单一职责原则和接口隔离原则。

  1. SaveOrderUseCase 仅仅用到了Repository里面的SaveOrder方法,但是却同时依赖了SearchOrder接口,这违反了接口隔离原则。
  2. 搜索用户的逻辑变更需要修改RepositoryImpl,可能会影响到SaveOrder的逻辑,这违反了单一职责原则。

优势:后置的接口隔离

go语言在接口隔离原则上有一个很大的优势,就是可以不依赖Repository的设计。

SaveOrderUseCase完全可以自己定义一个SaveOrderRepository,里面只有SaveOrder的方法,这时候RepositoryImpl依然是满足SaveOrderRepository的,可以直接拿来注入到SaveOrderUseCase中,如下图。

go语言在solid原则中的优势_第6张图片

  • DIP: 依赖倒置原则。
调用方不应该依赖于被调用方的实现,而应该依赖于接口。

指南: 依赖接口而不是具体的实现。

依赖倒置不是依赖注入,更不是自动注入。这里稍微解释一下

  • 依赖倒置: 应该依赖接口而不是具体实现。
  • 依赖注入: 依赖了接口以后,在运行以前必须的注入这个接口的具体实现。
  • 自动注入: 根据接口类型,名字等,通过技术手段找到合适的对象注入。

在上面的接口隔离的例子中就是遵守了依赖倒置的,我们可以看到usecase里面没有依赖具体的RepositoryImpl,而是依赖了Repository接口。

go优势:可以倒置没有接口的实现

加入Repository是一个外部的包,而且这个包同时实现了SaveOrder和SearchOrder两个方法,但是没有定义任何接口,这时候go语音依然是可以拿来使用的,如下图。只需要依赖方自己定义一个SaveOrderRepository方法即可。实时上,通过自己在包内定义接口的方式,可以保持非常好的“正交性”,让每个包直接减少任何依赖,包括接口层面的依赖。

go语言在solid原则中的优势_第7张图片

你可能感兴趣的:(golang,clean-code,arch)