内容简介:在软件中,衡量对象、包、函数任何两个部分相互依赖的程度叫做耦合。 例如下面的代码:缺少任何一方就无法存在这两个对象,编译更会报错。因此,它们被认为是紧密耦合的。紧密耦合的代码有许多不利的影响,但最重要的是它可能会引起代码散弹式的修改。散弹式的修改(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 开始,如下:
如你所见,它们是紧密耦合的; 如果没有 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
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 降低模块间耦合
- 原生骨架库模版功能上线,零耦合。
- 使用 spring 的 IOC 解决程序耦合
- 通过剔除上下文依赖减弱封装的耦合性
- 谈微服务间接口强耦合问题解决(200706)
- v8 cve-2019-5791:模块耦合导致的类型混淆
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。