微信号:RunningGeek

介绍:关于情怀,关于成长与分享,关于技术和开源.

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

2016-09-21 20:00 giraffe

▲点击上方“奔跑吧极客”关注吧~~


博客原文链接:http://yemengying.com/2016/09/11/solid-go-design-1/


原文链接: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

S.O.L.I.D

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 McIlroy 的 UNIX 哲学都是不完整的。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 语言中是如何应用的呢?


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 类型对扩展开放非常鹅妹子嘤的手段。


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 类型中的方法比普通函数多了一点语法糖—-将接收者作为一个预先声明的形参。(译者注:这块理解了好久😖。。。)


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


 
奔跑吧极客 更多文章 【译】S.O.L.I.D 原则在 Go 中的应用(下) 如何线程安全的使用 HashMap [译]如何给变量取个简短且无歧义的名字
猜您喜欢 Ruby: 一些简单实用的函数式编程实践 Mulgore Pro计划 Python学习之正则表达式 Android开发人员不得不收集的代码 【案例】百度、阿里、IBM...十大互联网巨头在用大数据做什么