Go 語言如何解決代碼耦合

什麼是耦合?

在軟件中,衡量對象、包、函數任何兩個部分相互依賴的程度叫作耦合。 例以下面的代碼:數據庫

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

設計模式原則-依賴倒轉(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)接口不該該依賴於結構體。與前一點相似,這是關於需求的特殊性。若是咱們定義咱們的接口爲: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 語言,咱們能夠進一步解耦這些對象。
在此以前,咱們還應該注意另外一個問題。 您可能已經注意到,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

相關文章
相關標籤/搜索