内容简介:在软件中,衡量对象、包、函数任何两个部分相互依赖的程度叫做耦合。 例如下面的代码:缺少任何一方就无法存在这两个对象,编译更会报错。因此,它们被认为是紧密耦合的。紧密耦合的代码有许多不利的影响,但最重要的是它可能会引起代码散弹式的修改。散弹式的修改(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:模块耦合导致的类型混淆
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Designing Data-Intensive Applications
Martin Kleppmann / O'Reilly Media / 2017-4-2 / USD 44.99
Data is at the center of many challenges in system design today. Difficult issues need to be figured out, such as scalability, consistency, reliability, efficiency, and maintainability. In addition, w......一起来看看 《Designing Data-Intensive Applications》 这本书的介绍吧!