【译】S.O.L.I.D 原则在 Go 中的应用(上)

文章目录
  1. 1. 世界上有多少个 Go 语言开发者?
  2. 2. Code review
  3. 3. Bad code
  4. 4. Good design
  5. 5. SOLID
  6. 6. 单一责任原则
    1. 6.1. 耦合 & 内聚
    2. 6.2. 包名
    3. 6.3. 不好的包名
    4. 6.4. Go 中的 UNIX 哲学
  7. 7. 开放封闭原则

最近两个月没有好好的看书学习,导致博客也水了两个月没写什么正经的。上周收到仓鼠🐹君萌萌哒的邮件之后,又激起了我写博客的欲望。由于自己最近灵感枯竭,所以我决定翻译一篇别人的O(∩_∩)O~。作为一个一直想学 Go,但想了好久还没入门的人,我挑了篇写 Go 的,顺便帮自己熟悉一下 Go。原文是作者根据自己 GolangUK 的演讲所整理的,全文以 SOLID 原则为线路讲述了什么样的 Go 代码才算是好代码,当然 SOLID 原则也适用于其他语言。原文比较长,所以准备分成上下两部分,也有十分非常以及特别大的可能是上中下(捂脸)。

咳咳,我果然是打脸体质,下翻译了一句就放弃了。不过,我把它交给了超靠谱的小伙伴。想看下的请移步【译】S.O.L.I.D 原则在 Go 中的应用(下)

捂。。。。。。。。还是不捂了,脸已经丢没了🙈

原文链接:http://dave.cheney.net/2016/08/20/solid-go-design?utm_source=wanqu.co&utm_campaign=Wanqu+Daily&utm_medium=website
原文作者:Dave Cheney

世界上有多少个 Go 语言开发者?

介个世界上有多少 Go 开发者捏?在脑海中想一个数字,我们会在最后回到这个话题。
thinking

Code review

有多少人将 code review 当做自己工作的一部分?[听演讲的人都举起了手]。为什么要做 code review?[一些人回答为了阻止不好的代码]

如果 code review 是为了捕捉到不好的代码,那么问题来了,你怎么判断你正在 review 的代码是好还是不好呢?

我们可以很容易的说出“这代码好辣眼睛”或者“这源码写的太吊了”,就像说“这画真美”,“这屋子真大气”一样。但是这些都是主观的,我希望找到一些客观的方法来衡量代码是好还是不好。

Bad code

下面看一下在 code review 中,一段代码有哪些特点会被认为是不好的代码。

  • Rigid 代码是不是很僵硬?是否由于严格的类型和参数导致修改代码的成本提高
  • Fragile 代码是不是很脆弱?是否一点小的改动就会造成巨大的破坏?
  • Immobile 代码是否难以重构?
  • Complex 代码是否是过度设计?
  • Verbose 当你读这段代码时,能否清楚的知道它是做什么的?

👆这些都不是什么好听的词,没有人希望在别人 review 自己代码时听到这些词。

Good design

了解了什么是不好的代码之后,我们可以说“我不喜欢这段代码因为它不易于修改”或者“这段代码并没有清晰的告诉我它要做什么”。但这些并没有带来积极的引导。

如果我们不仅仅可以描述不好的设计,还可以客观的描述好的设计,是不是更有助于提高呢。
excited

SOLID

2002年,Robert Martin 出版了《敏捷软件开发:原则、模式与实践》一书,在书中他描述了可重用软件设计的五个原则,他称之为 SOLID 原则(每个原则的首字母组合在一起)。

  • 单一责任原则
  • 开放封闭原则
  • 里氏替换原则
  • 接口分离原则
  • 依赖倒置原则

这本书有点过时了,书中谈论的语言都已经超过了十年之久。尽管如此,在谈论什么样的 Go 代码才是好代码时,SOLID 的原则依然可以给我们一些启发。

So,这也就是我花时间想在本文和大家一起讨论的。

单一责任原则

忙成狗
SOLID 原则中的第一个原则就是单一责任原则Robert C Martin 说过 A class should have one, and only one, reason to change(修改某个类的时候,原因有且只有一个),说白了就是,一个类只负责一项职责。

虽然 Go 语言中并没有类的概念–但我们有更鹅妹子嘤的 composition (组合)的特性。

为什么修改一段代码只负责一项职责如此重要呢?如果一个类有两个职责R1,R2,那么修改R1时,可能会导致也要修改R2。修改代码是痛苦的,但更痛苦的是修改代码的原因是由于修改其他代码引起的。

所以当一个类只负责一个功能领域中的相应职责时,可以修改的它的原因也就最大限度的变少了。

耦合 & 内聚

这两个词是用来形容一段代码是否易于修改的。

耦合是指两个东西需要一起修改—对其中一个的改动会影响到另一个。

另一个相关但独立的概念是内聚,一般指相互吸引的迷之力量。

在软件开发领域中,内聚常常用来描述一段代码内各个元素彼此结合的紧密程度。

下面我准备从 Go 的包模型开始,聊聊 Go 开发中的耦合与内聚。

包名

在Go中,所有代码都必须有一个所属的包。一个包名要描述它的用途,同时也是命名空间的前缀。下面是 Go 标准库中一些好的包名:

  • net/http,提供 http 的客户端和服务端。
  • os/exec,可以运行运行外部命令。
  • encoding/json,实现了 JSON 文件的编码和解码。
不好的包名

现在让我们来喷一些不好的包名。这些包名并没有很好的展现出它们的用途,当然了前提是它们有-_-|||。

  • package server 是提供什么?。。。好吧就当是提供一个服务端吧,但是是什么协议呢?
  • package private 是提供什么?一些我不应该看👀的东西?
  • 还有 package common, package utils,同样无法清楚的表达它们的用途,开发者也不易保持它们功能的专一性。

上面这些包很快就会变成堆放杂七杂八代码的垃圾堆,而且会由于功能太杂乱而频繁修改。

Go 中的 UNIX 哲学

在我看来,任何关于解耦设计的讨论如果没有提到 Doug McIlroyUNIX 哲学都是不完整的。UNIX 哲学就是主张将若干简洁,清晰的模块组合起来完成复杂的任务,而且通常情况下这个任务都不是原作者所能预想到的。

我想 Go 中的包正体现了 UNIX 哲学的精神。因为每一个包都是一个拥有单一责任的简洁的 Go 程序。

开放封闭原则

open or close
第二个原则,也就是 SOLID 当中的 O,是由 Bertrand Meyer 提出的开放封闭原则。1988年,Bertrand Mey 在他的著作《面向对象软件构造》一书中写道:Software entities should be open for extension,but closed for modification(软件实体应当对扩展开放,对修改关闭)。

那么这个n年前的建议在 Go 语言中是如何应用的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main
import (
"fmt"
)
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,B中嵌入(embedding)了类型A,并且B提供了他自己的 Greet 方法,覆盖了A的。

嵌入不仅仅是针对方法,还可以通过嵌入使用被嵌入类型的属性。我们可以看到,在上面的例子中,因为A和B定义在同一个包中,所以B可以像使用自己定义的属性一样使用A中的 private 的属性 year。

所以,嵌入是实现 Go 类型对扩展开放非常鹅妹子嘤的手段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main
import (
"fmt"
)
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 (c OctoCat) Legs() int {
return 5
}
func main() {
var octo OctoCat
fmt.Printf("I have %d legs\n", octo.Legs())// I have 5 legs
octo.PrintLegs()// I have 4 legs
}

在这个例子中,我们有一个 Cat 类型,它拥有一个 Legs 方法可以获得腿的数目。我们将 Cat 类型嵌入到一个新类型 OctoCat 中,然后声明 Octocat 有5条腿。然而,尽管 OctoCat 定义了它自己的 Legs 方法返回5,在调用 PrintLegs 方法时依旧会打印“I have 4 legs”。

这是因为 PrintLegs 方法是定义在 Cat 类型中的,它将 Cat 作为接收者,所以会调用 Cat 类型的 Legs 方法。Cat 类型并不会感知到它被嵌入到其他类型中,所以它的方法也不会被更改。

所以,我们可以说 Go 的类型是对扩展开放,对修改关闭的。

实际上,Go 类型中的方法比普通函数多了一点语法糖—-将接收者作为一个预先声明的形参。(译者注:这块理解了好久😖。。。,不懂得可以看这篇参考文档)

1
2
3
4
5
6
7
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 类型。这也将引出下一个原则—里氏替换原则。

且听下回分解。。。。。。。

——————————————别看我,我只是个傲娇的分割线———————————————————————

终于完成了上的部分↖(^ω^)↗,尽量在下周完成下。由于并不了解 Go 难免会有错误或翻译生硬的地方,欢迎指正错误,欢迎一起讨论~(≧▽≦)/~。

都看到这了,关注个公众号再走吧🙈
Running Geek

分享到 评论