首頁 > 軟體

分散式系統實踐解讀丨詳解高內聚低耦合

2020-09-22 16:30:34
摘要:做好高內聚低耦合,思路也很簡單:定職責、做歸類、劃邊界。

下面的這個場景你可能會覺得很熟悉(Z哥我又要出演了):

Z哥:@All 兄弟姐妹們,這次我這邊有個需求需要給「商品上架」增加一道稽核,會影響到大家和我互動的介面。大家抽空配合改一下,明天一起更新個版本。

小Y:哥,我這幾天很忙啊,昨天剛配合老王改過促銷!

小X:行~當一切已成習慣。

作為被通知人,如果在你的現實工作中也發生了類似事件,我相信哪怕嘴上不說,心裡也會有不少想法和抱怨:“md,改的是你,我也要釋出,好冤啊!”。

這個問題的根本原因就是多個項目之間的耦合度過於嚴重。

越大型的項目越容易陷入到這個昭潭中,難以自拔。

而解決問題的方式就是進行更合理的分層,並且持續保證分層的合理性。

一提到分層,必然離不開6個字「高內聚」和「低耦合」。

什麼是高內聚低耦合

在z哥之前的文章中有多次提到,分散式系統的本質就是「分治」和「冗餘」。

其中,分治就是“分解 -> 治理 -> 歸併”的三部曲。「高內聚」、「低耦合」的概念就來源於此。

需要注意的是,當你在做「分解」這個操作的時候,務必要關注每一次的「分解」是否滿足一個最重要的條件:不同分支上的子問題,不能相互依賴,需要各自獨立。

因為一旦包含了依賴關係,子問題和父問題之間就失去了可以被「歸併」的意義。

比如,一個「問題Z」被分解成了兩個子問題,「子問題A」和「子問題B」。但是,解問題A依賴於問題B的答案,解問題B又依賴於問題A的答案。這不就等於沒有分解嗎?

題外話:這裡的“如何更合理的分解問題”這個思路也可以用到你的生活和工作中的任何問題上。

所以,當你在做「分解」的時候,需要有一些很好的著力點去切入。

這個著力點就是前面提到的「耦合度」和「內聚度」,兩者是一個此消彼長的關係。

越符合高內聚低耦合這個標準,程式的維護成本就越低。為什麼呢?因為依賴越小,各自的變更對其他關聯方的影響就越小。

所以,「高內聚」和「低耦合」是我們應當持續不斷追求的目標。

題外話:耦合度,指的是軟體模組之間相互依賴的程度。比如,每次呼叫方法 A 之後都需要同步呼叫方法 B,那麼此時方法 A 和 B 間的耦合度是高的。

內聚度,指的是模組內的元素具有的共同點的相似程度。比如,一個類中的多個方法有很多的共同之處,都是做支付相關的處理,那麼這個類的內聚度是高的。

怎麼做好高內聚低耦合

做好高內聚低耦合,思路也很簡單:定職責、做歸類、劃邊界。

首先,定職責就是定義每一個子系統、每一個模組、甚至每一個class和每一個function的職責。

比如,在子系統或者模組層面可以這樣。

又比如,在class或者function層面可以這樣。

我想這點大家平時都會有意識的去做。

做好了職責定義後,內聚性就會有很大的提升,同時也提高了程式碼/程式的複用程度。

至此,我們才談得上「單一職責(SRP)」這種設計原則的運用。

其次,做歸類。梳理不同模組之間的依賴關係。

像上面提到的案例1可以歸類為3層:

  1. 基礎層:商品基礎服務、會員基礎服務、促銷基礎服務
  2. 聚合層:購物車服務、商品詳情服務、登陸服務
  3. 接入層:快閃店API、綜合商城API

案例2也可以歸類為3層:

  1. 資料訪問層:訪問會員表資料、訪問會員積分表資料、訪問會員等級表資料
  2. 業務邏輯層:會員登陸邏輯、會員使用積分邏輯、會員升級邏輯
  3. 應用層:接收使用者輸入的賬戶密碼、接收使用者輸入的使用積分數、接收使用者的付款資訊

最後就是劃邊界。好不容易梳理清楚,為了避免輕易被再次破壞,所以需要設立好合理清晰的邊界。

否則你想的是這樣整齊。

實際會慢慢變成這樣混亂。

那麼應該怎麼劃邊界呢?

class和function級別。這個層面可以通過codereview或者靜態程式碼檢測工具來進行,可以關注的點比如:

1.呼叫某些class必須通過interface而不是implement

2.訪問會員表資料的class中不能存在訪問商品資料的function

模組級別。可以選擇以下方案:

1.給每一種類型的class分配不同project,打包到各自的dll(jar)中

2.每次程式碼push上來的時候檢測其中的依賴是否有超出規定的依賴。例如,不能逆向依賴(檢測dal是否包含bll);不能在基礎層做聚合業務(檢測商品基礎服務是否包含其他基礎服務的dll(jar))。

系統級別。及時識別子系統之間的呼叫是否符合預期,可以通過接入一個呼叫鏈跟蹤系統(如,zipkin)來分析請求鏈路是否合法。

讓邊界更清晰、穩定的最佳實踐

很多時候不同的模組或者子系統會被分配到不同的小組中負責,所以z哥再分享幾個最佳實踐給你。它可以讓系統之間的溝通更穩定。

首先是:模組對外暴露的介面部分,資料類型的選擇上儘量做到寬進嚴出。比如,使用long代替byte之類的資料類型;使用弱類型代替強類型等等。

舉個「寬進嚴出」的例子:

//使用long代替byte之類的資料類型。
void Add(long param1, long param2){
    if(param1 <1000&& param2 < 1000){  //先接收進來,到裡面再做邏輯校驗。
        //do something...
    }
    else{
        //do something...
    }
}

其次是:寫操作介面,接收參數儘可能少;讀操作介面,返回參數儘可能多。

為什麼呢?因為很多時候,寫操作的背後會存在一個潛在預期,是「準確」。

準確度和可信度有著很大的聯絡,只有更多的邏輯處理在自己掌控範圍內進行才能越具備「可信度」(當然是職責範圍內的邏輯,而不是讓商品服務去計算促銷的邏輯)。反之,上游系統一個bug就會牽連到你的系統中。

而讀操作背後的潛在預期是:「滿足」。你得提供給我滿足我當前需要的資料,否則我的工作無法開展。

但是呢,在不同時期,客戶端所需要的資料可能會發生變化,你無法預測。所以呢,不要吝嗇,返回參數儘可能多,用哪些,用不用是客戶端的事。

還可以做的更好的一些,就是,在可以滿足的基礎上支援按需獲取。客戶端需要返回哪些欄位自己通過參數傳過來,如此一來還能避免浪費資源做無用的資料傳輸。

題外話:對外露出的介面設計,可以使用http + json 這種跨平臺 + 弱類型的技術組合,可具備更好的靈活性。

實際上,一個程式大多數情況下,在某些時刻是客戶端,又在某些時刻是服務端。站在一個完整程式的角度來提煉參數設計的思路就是:“吃”的要少,“產出”的要多。

題外話:有一些設計原則可以擴展閱讀一下。

單一職責原則SRP(Single Responsibility Principle)

開放封閉原則OCP(Open-Close Principle)

裡式替換原則LSP(the Liskov Substitution Principle LSP)

依賴倒置原則DIP(the Dependency Inversion Principle DIP)

介面分離原則ISP(the Interface Segregation Principle ISP)

總結

本文z哥帶你梳理了一下「高內聚低耦合」的本質(來自於哪,意義是什麼),並且分享了一些該怎麼做的思路。

可以看到「高內聚」、「低耦合」其實沒有這個名字那麼高階。哪怕你現在正在工作的項目是一個單體應用,也可以在class和function的設計中體會到「高內聚」、「低耦合」的奧妙。

來來來,接下去馬上開始在項目中「刻意練習」起來吧~

 

點選關注,第一時間瞭解華為雲新鮮技術~


IT145.com E-mail:sddin#qq.com