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

最近两个月没有好好的看书学习,导致博客也水了两个月没写什么正经的。上周收到仓鼠🐹君萌萌哒的邮件之后,又激起了我写博客的欲望。由于自己最近灵感枯竭,所以我决定翻译一篇别人的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 开发者捏?在脑海中想一个数字,我们会在最后回到这个话题。

《S.O.L.I.D 原则在 Go 中的应用(上)》

Code review

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

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

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

Bad code

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

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

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

Good design

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

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

《S.O.L.I.D 原则在 Go 中的应用(上)》

SOLID

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

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

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

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

单一责任原则

《S.O.L.I.D 原则在 Go 中的应用(上)》

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 程序。

开放封闭原则

《S.O.L.I.D 原则在 Go 中的应用(上)》

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

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

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()
	b.Greet()
	}

上面的代码中,我们有类型A,包含属性 year 和一个方法 Greet。我们还有类型B,B中嵌入(embedding)了类型A,并且B提供了他自己的 Greet 方法,覆盖了A的。

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

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

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())
	octo.PrintLegs()
	}

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

这是因为 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 类型。这也将引出下一个原则—里氏替换原则。

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

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

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

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

《S.O.L.I.D 原则在 Go 中的应用(上)》

    原文作者:算法小白
    原文地址: https://juejin.im/entry/57e4d6670e3dd900580c8716
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞