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 。 这让我想到了下一个原则。