在软件中,衡量对象、包、函数任何两个部分相互依赖的程度叫作耦合。 例以下面的代码:数据库
type Config struct {
DSN string
MaxConnections int
Timeout time.Duration
}
type PersonLoader struct {
Config *Config
}
复制代码
缺乏任何一方就没法存在这两个对象,编译更会报错。所以,它们被认为是紧密耦合的。json
紧密耦合的代码有许多不利的影响,但最重要的是它可能会引发代码散弹式的修改。散弹式的修改(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 对象添加密码字段会发生什么。假设咱们不但愿该字段做为API 响应的一部分输出。而后,咱们必须在 outputAsCSV(), outputAsXML() 和outputAsJSON() 函数中引入其余代码。
这一切彷佛合理的,可是若是咱们还有另外一个入口也包含 User 类型做为其输出的一部分,如“Get All Users”入口,会发生什么?这会使咱们也必须在那里作出相似的改变。这是由于“Get All Users”入口与用户类型的输出呈现紧密耦合。
另外一方面,若是咱们将渲染逻辑从 GetUserHandler() 移动到 User 类型,那么咱们只有一个地方能够进行更改。也许更重要的是,这个地方很明显且很容易找到,由于它位于咱们添加新字段的位置旁边,从而提升了整个代码的可维护性。app
依赖倒置原则是 Robert C. Martin 在 1996 年发表的题为“依赖性倒置原则”的 C ++ 报告的文章中创造的术语。他将其定义为:高级模块不该该依赖低级模块。二者都应该取决于抽象。抽象不该该依赖于细节。细节应取决于抽象。
Robert C. Martin 寥寥数语却极具智慧,如下是我将其转化为 Go 语言对应结论:
1)高层次包不该该依赖于低层次包。当咱们编写一个 Go 语言应用程序时,从 main() 调用一些包,这些能够被认为是高级包。相反一些包与外部资源交互的包,如数据库,一般不是从 main() 调用,而是从业务逻辑层调用,而业务逻辑层会低1-2级。 关于这一点,高层次的包不该该依赖于低级别的包。高级包依赖于抽象,而不是依赖于这些基本细节实现的包。从而保持它们分离。
2)结构体不该该依赖于结构体。当一个结构体使用另外一个结构体做为方法输入或成员变量时:函数
type PizzaMaker struct{}
func (p *PizzaMaker) MakePizza(oven *SuperPizaOven5000) {
pizza := p.buildPizza()
oven.Bake(pizza)
}
复制代码
将这两个结构分离是不可能的,这些对象是紧密耦合的,所以不是很灵活。考虑这个真实的例子:假设我走进旅行社问,我能够订澳洲航空公司星期四下午三点半飞往悉尼的 15D 座位吗?旅行社将很难知足个人要求。 可是若是我放宽要求,改成询问我能够订一张周四飞往悉尼的机票吗?这样旅行社的生活就更灵活了,我也更有可能获得个人座位。更新咱们的代码以下:测试
type PizzaMaker struct{}
func (p *PizzaMaker) MakePizza(oven Oven) {
pizza := p.buildPizza()
oven.Bake(pizza)
}
type Oven interface {
Bake(pizza Pizza)
}
复制代码
如今咱们可使用任何实现 Bake() 方法的对象。
3)接口不该该依赖于结构体。与前一点相似,这是关于需求的特殊性。若是咱们定义咱们的接口为:ui
type Config struct {
DSN string
MaxConnections int
Timeout time.Duration
}
type PersonLoader interface {
Load(cfg *Config, ID int) *Person
}
复制代码
而后咱们将 PersonLoader 与指定的 Config 结构体解耦。加密
type PersonLoaderConfig interface {
DSN() string
MaxConnections() int
Timeout() time.Duration
}
type PersonLoader interface {
Load(cfg PersonLoaderConfig, ID int) *Person
}
复制代码
如今,咱们能够重用 PersonLoader 而无需任何更改。
(上面的结构应该被认为是指提供逻辑和/或实现接口的结构,而且不包括用做数据传输对象的结构)spa
抛弃全部的背景,让咱们用更为丰富的例子深刻探讨如何解决紧密耦合代码。 咱们的示例从两个不一样包中的两个对象,Person 和 BlueShoes 开始,以下: 设计
正如原文做者 Go 语言依赖注入实践 一书中所写,Unix 哲学是 Go 语言中最受欢迎的概念之一,其中指出:“Write programs that do one thing and do it well. Write programs to work together.”
意思是把不一样需求进行区分,让你的每份代码只作一件事情而且作好,使彼此之间相互配合工做。
这些概念在 Go 标准库中无处不在,甚至出如今语言的设计决策中。像隐式实现接口(即没有“implements”关键字)。这样的决策使咱们(该语言的用户)可以实现解耦代码,这些代码能够用于单一目的而且易于编写。
轻耦合代码使理解更为容易,由于你所须要的全部信息都集中在一个地方,这会让测试和扩展变得很是轻松。
因此当你下次看到一个具体的对象做为函数参数或成员变量时,问问你本身这是必要的吗?若是我将其更改成接口,会更灵活、更易于理解或更易于维护吗?
govip cn 每日新闻推荐的文章 how-to-fix-tightly-coupled-go-code