如何对Go代码解偶

栏目: Go · 发布时间: 5年前

内容简介:你是否曾经由于添加某个新功能却导致另一个功能出问题?修复好这个,另一个又出问题,就好象打地鼠一般。你是否曾经花费大量的时间调试Bug,最后却发现问题潜伏在另一个、甚至毫不相关的模块中?

原文 ,文章作者也是《Hands-On Dependency Injection in Go》的作者,向原作者表示感谢。

你是否曾经由于添加某个新功能却导致另一个功能出问题?修复好这个,另一个又出问题,就好象打地鼠一般。

你是否曾经花费大量的时间调试Bug,最后却发现问题潜伏在另一个、甚至毫不相关的模块中?

这些问题都是由于高耦合引起的。

在这篇文章中,我们来使用解偶的方式来让代码更加易于理解、维护、测试。

什么是耦合?

在软件开发中,耦合是用来测量2个部分(对象、包、函数)之间相互依赖程度的指标。参考下面的例子:

type Config struct {
	DSN            string
	MaxConnections int
	Timeout        time.Duration
}

type PersonLoader struct {
	Config *Config
}

这2个对象不能离开另一个单独存在(Roy个人理解:虽然可以单独声明Config对象,但单独这个对象是什么也做不了的。),所以说这2者是高耦合的(tightly coupled)。

为什么高耦合的代码是个问题?

高耦合代码会带来很多问题,但最明显的一个就是会导致散弹式修改(shotgun surgery)。散弹式修改是一个术语,用来描述那种修改一个地方的代码导致要修改很多其他地方代码的情况。

考虑下面的代码:

func GetUserEndpoint(resp http.ResponseWriter, req *http.Request) {
	// get and check inputs
	ID, err := getRequestedID(req)
	if err != nil {
		resp.WriteHeader(http.StatusBadRequest)
		return
	}

	// load requested data
	user, err := loadUser(ID)
	if err != nil {
		// technical error
		resp.WriteHeader(http.StatusInternalServerError)
		return
	}
	if user == nil {
		// user not found
		resp.WriteHeader(http.StatusNoContent)
		return
	}

	// prepare output
	switch req.Header.Get("Accept") {
	case "text/csv":
		outputAsCSV(resp, user)

	case "application/xml":
		outputAsXML(resp, user)

	case "application/json":
		fallthrough

	default:
		outputAsJSON(resp, user)
	}
}

现在假设我们要在User对象中添加一个Password字段,但我们不想让这个字段出现在API返回数据中,我们不得不同时修改 outputAsCSV()outputAsXML()outputAsJSON() 这3个函数。

这看上去似乎没什么问题,但试想如果我们还有其他的接口(endpoint)也将User作为输出的一部分,比如”GetAllUsers”,我们不得不做同样的工作。这一切都是由于”GetUser”接口和User类型输出的形式是高耦合的。

换言之,我们把渲染逻辑从”GetUserHandler”接口转移到User类型中,这样我们修改一个地方就可以了。更重要的是,如果我们需要添加新的字段,User类显然更加明显和容易找到,这样也提高了整体代码的可维护性。

在我们深入讨论如何解偶之前,我们还需要讨论一下依赖倒置原则(Dependency Inversion Principle)。

依赖倒置原则

依赖倒置原则(DIP)由Robert C. Martin在1996年在 Dependency Inversion Principle 中提出,他对此定义如下:

顶层模块不应该依赖底层模块。这2者都应该依赖于抽象。抽象不应该依赖于细节。细节应该依赖于抽象。

High level modules should not depend on low level modules. Both should depend on abstractions. Abstractions should not depend upon details. Details should depend on abstractions

Robert C. Martin

(Roy注:为了统一下面把modules、packages都翻译成模块。)

正如Robert所言,大智慧往往浓缩成非常精炼的句子。下面是我的理解并且翻译成 Go 代码:

1) 顶层模块不应该依赖底层模块 - 当我们编写Go程序时,一些包被 main() 函数调用,这些可以认为是顶层模块。相反的,一些和外部资源交互的模块,比如数据库,典型的不由 main() 调用而是由逻辑层调用,这就要低1-2级。

顶层模块应该依赖于抽象而不是这些实现细节的模块,这有助于解偶。

2) 结构体不应该依赖于结构体 - 当一个结构体接受另一个结构体作为方法参数或成员变量时:

type PizzaMaker struct{}

func (p *PizzaMaker) MakePizza(oven *SuperPizaOven5000) {
	pizza := p.buildPizza()
	oven.Bake(pizza)
}

这样的代码耦合度很高导致及其不灵活,让我们考虑一个实际情况:我走进一家旅行社并说:”我想购买一张澳洲航空在星期四3点30分飞往悉尼的15D座位机票。”这种说法旅行社是很难满足我的要求的。

但如果我放松要求,就好像我们将输入的参数由结构体类型改成接口类型一般:”我想购买一张星期四飞往悉尼的机票。”这样旅行社可以更加灵活处理我的请求,也就更可能买到机票。

修改代码如下:

type PizzaMaker struct{}

func (p *PizzaMaker) MakePizza(oven Oven) {
	pizza := p.buildPizza()
	oven.Bake(pizza)
}

type Oven interface {
	Bake(pizza Pizza)
}

这样就可以使用任何类型来实现 Bake() 方法。

3) 接口不应该依赖结构体 - 和前文类似,根据特定的情况,我们定义接口:

type Config struct {
	DSN            string
	MaxConnections int
	Timeout        time.Duration
}

type PersonLoader interface {
	Load(cfg *Config, ID int) *Person
}

我们将 PersonLoaderConfig 这个特定的额结构体绑定到了一起,也就是说想在其他项目中复用 PersonLoader 将需要修改代码,而这些修改可能导致Bug。换句话说,如果我们像下面这样定义 PersonLoader

type PersonLoaderConfig interface {
	DSN() string
	MaxConnections() int
	Timeout() time.Duration
}

type PersonLoader interface {
	Load(cfg PersonLoaderConfig, ID int) *Person
}

这样我们就可以在其他地方复用 PersonLoader 了。

上面的结构体应该用作提供逻辑实现接口而不是用作传递数据。(原文’Structs above should be taken to mean structs that provide logic and/or implement interfaces and does not include structs that are used as Data Transfer Objects’,翻译的有点别扭。)

解偶

了解背景后,我们来深入了解一下如何解偶。这个例子中我们有2个对象, PersonBlueShoes 分别在2个不同的包中:

如何对Go代码解偶

正如图中所示,它们是高耦合的, Person 没法离开 BlueShoes 单独存在。

如果你之前使用Java/C++,本能的解偶方法是在 shoes 包中定义一个接口:

如何对Go代码解偶

在大多数语言中到此为止了,然而在Go中,我们可以更进一步。

在我们这么做之前,需要注意一个问题。

你也许注意到了, Person 结构只实现了一个 Walk() 方法,而在 Footwear 中实现了 Walk()Run() 。这种差异导致了 PersonFootwear 之间的关系有些不明确并且违反了Robert C. Martin提出的另一个原则: 接口隔离原则(Interface Segregation Principle ,ISP)

使用者不应该被强迫依赖那些它们不使用的方法。

Clients should not be forced to depend on methods they do not use.

Robert C. Martin

幸运的是,我们可以同过在 people 包中而非 shoes 包中定义接口来解决这些问题:

如何对Go代码解偶

这也许看起来是个小事,不值得为此花费时间,但实际上这是意义深远的。在上面这个例子中2个包完全解偶了, people 再也不需要依赖或使用 shoes 包了。

通过这种改变, people 包更加简洁明了易于发现,而且以后修改 shoes 包不会影响到 people 包。

小结

正如我在《Hands-On Dependency Injection in Go》中写的,Go语言中一个流行的概念和 Unix哲学 相似:

Write programs that do one thing and do it well. Write programs to work together

(这句就不翻译了,翻译的没意境。)

这个概念充满在Go标准库中,甚至影响到了Go语言的设计。像隐式实现接口(即没有“implements”关键字)使我们(该语言的用户)能够实现解耦代码,这些代码可以用于单一目标并且易于编写。

低耦合的代码更易于理解,因为你需要的所有信息都在一个地方,反过来说使代码更容易测试和扩展。

所以下次你看到一个具体的对象作为函数参数或者成员变量,问问自己”这真的必要吗?”,”如果把这个改成接口类型,会不会更加灵活、易于理解和维护?”

Happy Coding!


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

破茧成蝶:用户体验设计师的成长之路

破茧成蝶:用户体验设计师的成长之路

刘津、李月 / 人民邮电出版社 / 2014-7 / 69.00

市面上已经有很多专业的用户体验书籍,但解决用户体验设计师在职场中遇到的众多现实问题的图书并不多见。本书从用户体验设计师的角度出发,系统地介绍了其职业生涯中的学习方法、思维方式、工作流程等,覆盖了用户体验设计基础知识、设计师的角色和职业困惑、工作流程、需求分析、设计规划和设计标准、项目跟进和成果检验、设计师职业修养以及需要具备的意识等,力图帮助设计师解决在项目中遇到的一些常见问题,找到自己的职业成长......一起来看看 《破茧成蝶:用户体验设计师的成长之路》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具