在軟件中,衡量對象、包、函數任何兩個部分相互依賴的程度叫作耦合。 例以下面的代碼:數據庫
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
依賴倒置原則是 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 語言,咱們能夠進一步解耦這些對象。正如原文做者 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