译 | SOLID Go Design(一)

简介: 译 | SOLID Go Design

Code review

在座的各位有谁把 code review 作为日常工作的一部分?【整个房间举起了手,鼓舞人心】。好的,为什么要进行 code review ?【有人高呼“阻止不良代码”】

如果代码审查是为了捕捉糟糕的代码,那么你如何知道你正在审查的代码是好还是糟糕?

正如你可能会说“这幅画很漂亮”或“这个房间很漂亮”,现在你可以说“代码很难看”或“源代码很漂亮”,但这些都是主观的。我正在寻找以客观方式谈论代码好或坏的特征。

Bad code

你在 code review 中可能会遇到以下这些糟糕代码的特征:

  • rigid - 代码死板吗?它是否有强类型或参数,以致难于修改?
  • fragile - 代码脆弱吗?细微的改变是否会在代码库中引起不可估量的破坏?
  • immobile - 代码难以重构吗?代码只需敲敲键盘就可以避免循环导入?
  • complex - 有没有代码是为了炫技,是否过度设计?
  • verbose - 代码使用费力吗?当阅读时,能看出来代码在做什么吗?

这些词是正向吗?你是否乐于看到这些词用于审核您的代码?

想必不会。

Good design

但这是一个进步,现在我们可以说“我不喜欢它,因为它太难修改”,或“我不喜欢它,因为我不知道代码试图做什么”,但如何正向引导呢?

如果有一些方法可以描述糟糕的设计,以及优秀设计的特征,并且能够以客观的方式做到这一点,那不是很好吗?

SOLID

2002年,Robert Martin 出版了他的书 agile software development, principles, patterns, and practices 其中描述了可重用软件设计的五个原则,并称之为 SOLID(英文首字母缩写)原则:

  • 单一职责原则(Single Responsibility Principle)
  • 开放/封闭原则(Open / Closed Principle)
  • 里氏替换原则(Liskov Substitution Principle)
  • 接口隔离原则(Interface Segregation Principle)
  • 依赖倒置原则(Dependency Inversion Principle)

这本书有点过时了,它所讨论的语言是十多年前使用的语言。但是,也许 SOLID 原则的某些方面可以给我们提供些线索,关于怎样谈论一个精心设计的 Go 程序。

单一职责原则(Single Responsibility Principle)

SOLID的第一个原则,S,是单一责任原则。

A class should have one, and only one, reason to change. – Robert C Martin

现在 Go 显然没有 classses - 相反,我们有更强大的组合概念 - 但是如果你能回顾一下 class 这个词的用法,我认为此时会有一定价值。

为什么一段代码只有一个改变的原因很重要?嗯,就像你自己的代码可能会改变一样令人沮丧,发现您的代码所依赖的代码在您脚下发生变化更痛苦。当你的代码必须改变时,它应该响应直接刺激作出改变,而不应该成为附带损害的受害者。

因此,具有单一责任的代码修改的原因最少。

Coupling & Cohesion

描述改变一个软件是多么容易或困难的两个词是:耦合和内聚。

  • 耦合只是一个词,描述了两个一起变化的东西 —— 一个运动诱导另一个运动。
  • 一个相关但独立的概念是内聚,一种相互吸引的力量。

在软件上下文中,内聚是描述代码片段之间自然相互吸引的特性。

为了描述Go程序中耦合和内聚的单元,我们可能会将谈谈函数和方法,这在讨论 SRP 时很常见,但是我相信它始于 Go 的 package 模型。

SRP: Single Responsibility Principle

Package names

在 Go 中,所有的代码都在某个 package 中,一个设计良好的 package 从其名称开始。包的名称既是其用途的描述,也是名称空间前缀。Go 标准库中的一些优秀 package 示例:

  • net/http - 提供 http 客户端和服务端
  • os/exec - 执行外部命令
  • encoding/json - 实现JSON文档的编码和解码

当你在自己的内部使用另一个 pakcage 的 symbols 时,要使用 import 声明,它在两个 package 之间建立一个源代码级的耦合。 他们现在彼此知道对方的存在。

Bad package names

这种对名字的关注可不是迂腐。命名不佳的 package 如果真的有用途,会失去罗列其用途的机会。

  • server package 提供什么? …, 嗯,希望是服务端,但是它使用哪种协议?
  • private package 提供什么?我不应该看到的东西?它应该有公共符号吗?
  • common package,和它的伴儿 utils package 一样,经常被发现和其他’伙伴’一起发现

我们看到所有像这样的包裹,就成了各种各样的垃圾场,因为它们有许多责任,所以经常毫无理由地改变。

Go’s UNIX philosophy

在我看来,如果不提及 Doug McIlroy 的 Unix 哲学,任何关于解耦设计的讨论都将是不完整的;小而锋利的工具结合起来,解决更大的任务,通常是原始作者无法想象的任务。

我认为 Go package 体现了 Unix 哲学的精神。实际上,每个 Go package 本身就是一个小的 Go 程序,一个单一的变更单元,具有单一的责任。

开放/封闭原则(Open / Closed Principle)

第二个原则,即 O,是 Bertrand Meyer 的开放/封闭原则,他在1988年写道:

Software entities should be open for extension, but closed for modification. – Bertrand Meyer, Object-Oriented Software Construction

该建议如何适用于21年后写的语言?

package main
type A struct {
        year int
}
func (a A) Greet() { fmt.Println("Hello GolangUK", a.year) }
type B struct {
        A
}
func (b B) Greet() { fmt.Println("Welcome to GolangUK", b.year) }
func main() {
        var a A
        a.year = 2016
        var b B
        b.year = 2016
        a.Greet() // Hello GolangUK 2016
        b.Greet() // Welcome to GolangUK 2016
}

我们有一个类型 A ,有一个字段 year 和一个方法 Greet。我们有第二种类型,B 它嵌入了一个 A,因为 A 嵌入,因此调用者看到 B 的方法覆盖了 A 的方法。因为A作为字段嵌入B ,B可以提供自己的 Greet 方法,掩盖了 A 的 Greet 方法。

但嵌入不仅适用于方法,还可以访问嵌入类型的字段。如您所见,因为A和B都在同一个包中定义,所以 B 可以访问 A 的私有 year 字段,就像在 B 中声明一样。

因此嵌入是一个强大的工具,允许 Go 的类型对扩展开放。

package main
type Cat struct {
        Name string
}
func (c Cat) Legs() int { return 4 }
func (c Cat) PrintLegs() {
        fmt.Printf("I have %d legs\n", c.Legs())
}
type OctoCat struct {
        Cat
}
func (o OctoCat) Legs() int { return 5 }
func main() {
        var octo OctoCat
        fmt.Println(octo.Legs()) // 5
        octo.PrintLegs()         // I have 4 legs
}

在这个例子中,我们有一个 Cat 类型,可以用它的 Legs 方法计算它的腿数。我们将 Cat 类型嵌入到一个新类型 OctoCat 中,并声明 Octocats 有五条腿。但是,虽然 OctoCat 定义了自己的 Legs 方法,该方法返回5,但是当调用 PrintLegs 方法时,它返回4。

这是因为 PrintLegs 是在 Cat 类型上定义的。 它需要 Cat 作为它的接收器,因此它会发送到 Cat 的 Legs 方法。Cat 不知道它嵌入的类型,因此嵌入时不能改变其方法集。

因此,我们可以说 Go 的类型虽然对扩展开放,但对修改是封闭的。

事实上,Go 中的方法只不过是围绕在具有预先声明形式参数(即接收器)的函数的语法糖。

func (c Cat) PrintLegs() {
        fmt.Printf("I have %d legs\n", c.Legs())
}
func PrintLegs(c Cat) {
        fmt.Printf("I have %d legs\n", c.Legs())
}

接收器正是你传入它的函数,函数的第一个参数,并且因为Go不支持函数重载,OctoCat不能替代普通的Cat 。 这让我想到了下一个原则。

目录
打赏
0
0
0
0
52
分享
相关文章
员工上网行为监控中的Go语言算法:布隆过滤器的应用
在信息化高速发展的时代,企业上网行为监管至关重要。布隆过滤器作为一种高效、节省空间的概率性数据结构,适用于大规模URL查询与匹配,是实现精准上网行为管理的理想选择。本文探讨了布隆过滤器的原理及其优缺点,并展示了如何使用Go语言实现该算法,以提升企业网络管理效率和安全性。尽管存在误报等局限性,但合理配置下,布隆过滤器为企业提供了经济有效的解决方案。
97 8
员工上网行为监控中的Go语言算法:布隆过滤器的应用
|
2月前
|
go语言中数组和切片
go语言中数组和切片
54 7
百炼-千问模型通过openai接口构建assistant 等 go语言
由于阿里百炼平台通义千问大模型没有完善的go语言兼容openapi示例,并且官方答复assistant是不兼容openapi sdk的。 实际使用中发现是能够支持的,所以自己写了一个demo test示例,给大家做一个参考。
|
1天前
|
公司局域网管理系统里的 Go 语言 Bloom Filter 算法,太值得深挖了
本文探讨了如何利用 Go 语言中的 Bloom Filter 算法提升公司局域网管理系统的性能。Bloom Filter 是一种高效的空间节省型数据结构,适用于快速判断元素是否存在于集合中。文中通过具体代码示例展示了如何在 Go 中实现 Bloom Filter,并应用于局域网的 IP 访问控制,显著提高系统响应速度和安全性。随着网络规模扩大和技术进步,持续优化算法和结合其他安全技术将是企业维持网络竞争力的关键。
14 1
公司局域网管理系统里的 Go 语言 Bloom Filter 算法,太值得深挖了
|
8天前
|
【02】客户端服务端C语言-go语言-web端PHP语言整合内容发布-优雅草网络设备监控系统-2月12日优雅草简化Centos stream8安装zabbix7教程-本搭建教程非docker搭建教程-优雅草solution
【02】客户端服务端C语言-go语言-web端PHP语言整合内容发布-优雅草网络设备监控系统-2月12日优雅草简化Centos stream8安装zabbix7教程-本搭建教程非docker搭建教程-优雅草solution
57 20
探秘员工泄密行为防线:基于Go语言的布隆过滤器算法解析
在信息爆炸时代,员工泄密行为对企业构成重大威胁。本文聚焦布隆过滤器(Bloom Filter)这一高效数据结构,结合Go语言实现算法,帮助企业识别和预防泄密风险。通过构建正常操作“指纹库”,实时监测员工操作,快速筛查可疑行为。示例代码展示了如何利用布隆过滤器检测异常操作,并提出优化建议,如调整参数、结合日志分析系统等,全方位筑牢企业信息安全防线,守护核心竞争力。
|
14天前
|
Go语言入门:分支结构
本文介绍了Go语言中的条件语句,包括`if...else`、`if...else if`和`switch`结构,并通过多个练习详细解释了它们的用法。`if...else`用于简单的条件判断;`if...else if`处理多条件分支;`switch`则适用于基于不同值的选择逻辑。特别地,文章还介绍了`fallthrough`关键字,用于优化重复代码。通过实例如判断年龄、奇偶数、公交乘车及成绩等级等,帮助读者更好地理解和应用这些结构。
35 14
|
2月前
|
go语言中结构体(Struct)
go语言中结构体(Struct)
126 71
|
2月前
|
go语言中的数组(Array)
go语言中的数组(Array)
123 67

热门文章

最新文章