Go 语言如何解决代码耦合

什么是耦合?

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

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 开始,如下:

如你所见,它们是紧密耦合的; 如果没有 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

转载于:https://juejin.im/post/5c5ae7fce51d457fc5647775

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值