Go 语言如何解决代码耦合

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

内容简介:在软件中,衡量对象、包、函数任何两个部分相互依赖的程度叫做耦合。 例如下面的代码:缺少任何一方就无法存在这两个对象,编译更会报错。因此,它们被认为是紧密耦合的。紧密耦合的代码有许多不利的影响,但最重要的是它可能会引起代码散弹式的修改。散弹式的修改(Shotgun Surgery)是指一部分的代码变化,导致在代码的其他地方需要根据变化情况,进行相应的修改。 请考虑以下代码:

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

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

type PersonLoader struct {
    Config *Config
}
复制代码

缺少任何一方就无法存在这两个对象,编译更会报错。因此,它们被认为是紧密耦合的。

为什么紧密耦合的代码有问题?

紧密耦合的代码有许多不利的影响,但最重要的是它可能会引起代码散弹式的修改。散弹式的修改(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 类型,那么我们只有一个地方可以进行更改。也许更重要的是,这个地方很明显且很容易找到,因为它位于我们添加新字段的位置旁边,从而提高了整个代码的可维护性。

设计模式原则-依赖倒转(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)接口不应该依赖于结构体。与前一点类似,这是关于需求的特殊性。如果我们定义我们的接口为:

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 而无需任何更改。

(上面的结构应该被认为是指提供逻辑和/或实现接口的结构,并且不包括用作数据传输对象的结构)

修复紧密耦合的代码

抛弃所有的背景,让我们用更为丰富的例子深入探讨如何解决紧密耦合代码。 我们的示例从两个不同包中的两个对象,Person 和 BlueShoes 开始,如下:

Go 语言如何解决代码耦合

如你所见,它们是紧密耦合的; 如果没有 BlueShoes,Person 结构就无法存在。 如果你像原文作者一样,有 Java/C++ 或者其他代码的经验,那么你将对象解耦的第一直觉就是在 Shoes Package 中定义一个接口。 结果会如下:

Go 语言如何解决代码耦合

在许多语言中,这将是它的最终结果。但是对于 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 中定义接口:

Go 语言如何解决代码耦合

这件小事也许不值得你珍惜宝贵的时间,但差异很大。 在这个例子中,我们两个 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


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

查看所有标签

猜你喜欢:

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

编程风格

编程风格

[美] Cristina Videira Lopes / 顾中磊 / 人民邮电出版社 / 2017-8 / 55.00元

本书对一个常见的编程问题定义了不同的约束,分别使用33种方法实现了同一个词频统计任务,从而形成了风格迥异的编程风格。作者以惯用的计算机语言与简单的任务为画笔,描绘了一次生动难忘的编程之旅,帮助读者加深了对语言的理解,也提供了崭新的编程思路。一起来看看 《编程风格》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具