Go 语言如何解决代码耦合

什么是耦合?

在软件中,衡量对象、包、函数任何两个部分相互依赖的程度叫作耦合。 例以下面的代码:数据库

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

设计模式原则-依赖倒转(Dependency Inversion Principle,DIP)

依赖倒置原则是 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 开始,以下: 设计

如你所见,它们是紧密耦合的; 若是没有 BlueShoes,Person 结构就没法存在。 若是你像原文做者同样,有 Java/C++ 或者其余代码的经验,那么你将对象解耦的第一直觉就是在 Shoes Package 中定义一个接口。 结果会以下:
在许多语言中,这将是它的最终结果。可是对于 Go 语言,咱们能够进一步解耦这些对象。
在此以前,咱们还应该注意另外一个问题。 您可能已经注意到,Person struct 只实现了一个 Walk() 方法,而 Footwear 同时实现了 Walk() 和 Run() 两个方法。这种差别使得 Person 和 Footwear 之间的关系有些不清楚,而且违反了 Robert C. Martin 提出的另外一个名为 Interface Segregation Principle(ISP) 的接口隔离原则,该原则指出:Clients should not be forced to depend on methods they do not use. 幸运的是,咱们能够经过在 People Package 中定义接口来解决这两个问题,而不是像上图中在 Shoes Package 中定义接口:
这件小事也许不值得你珍惜宝贵的时间,但差别很大。 在这个例子中,咱们两个 Package 如今彻底解耦了。People 不须要依赖或使用 Shoes Package。
经过这样更改使得 People Package 接口需求清晰,简洁且易于查找,由于它们位于示例包中,最后,对 Shoes Package 的更改不太可能影响 People Package。

总结

正如原文做者 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

相关文章
相关标签/搜索