2022-12-29|閱讀時間 ‧ 約 11 分鐘

談談軟體元件的內聚與耦合(2)

本篇文章將會繼續上一篇的內容,還沒看過上一篇的朋友,記得先看完上一篇再繼續本篇的閱讀唷。
前一篇我們提到了元件的內聚,以及用來衡量內聚性的三個標準,CCP、CRP與REP。
在這篇中,我們要來繼續談談軟體的耦合性。
元件的耦合是指元件與其他模組或元件之間的相依性,越大的話代表該元件與其他元件的相依性越大。
接下來在元件的耦合性中,以下這三個原則則是用來處理元件之間的關係。

無環依賴原則(AOP)

無環依賴原則的意義在於,在元件的依賴圖關係中不允許出現環
乍聽之下,似乎有點抽象,甚麼是環呢?
讓我們來看一下這張圖﹔
在右下角處,Authorizer、Interactors、Entities 形成了一個閉鎖的環狀依賴,我們就稱這個狀況為依賴環。
箭頭的方向也是有意義的,尖的地方代表被依賴者,沒有尖的地方則代表依賴者,舉例來說,Entities 中用到了 Authorizer 中的類別,所以方向會朝向Authorizer。
不過,依賴環會產生甚麼問題呢?
如果要測試 Entities 元件的話,我們必須要建置 Authorizer 和 Interactors,由於它是環狀依賴,使其很難確定元件建置的順序,這種環也使得我們非常難以對元件進行隔離。
如此以來,「隔天早上綜合症(Morning-after syndrome)」便會很常發生,當你今天下班前開心的把某個元件的 change 推上 code repo,結果隔天來發現你的系統突然壞了,原因是因為前一天還有人比你晚走,並改了你所依賴的某個函式庫。

解除依賴環
要解除元件之間的依賴環,並把其恢復為有像無環圖,可以使用下面這兩種方法。
依賴反向原則(Dependency-Inversion Principle) :
根據上面的情況,假設在 Entities 中的 User 類別用到了 Authorizer 中的類別,我們可以建立一個具有 User 所需物件的介面,並把其放入 Entities 中,讓Authorizer 中的該類別繼承它,由此而來便反向了 Entities 與 Authorizer 之間的依賴關係,解除了依賴環。
新建立一個 Entities 和 Authorizer 都依賴的元件,把 Entities 和 Authorizer 都依賴的類別移動到這個元件中。
兩種方法個人覺得其實概念上相當接近,都是想反轉 Authorzer 與 Entities 之間的依賴關係,差別在於是否要把依賴的元件獨立出來,這就回到了上一篇所講的元件內聚性,我們在評估反轉依賴時就要考量,是否有必要把元件獨立出來呢? 亦或者把介面抽離出來即可?
讀到這裡,我們就必須要了解:
  • High level modules should not depend upon low level modules. Both should depend upon abstrations. (高階模組不能相依於低階模組,而兩者都要相依於抽象)
  • Abstractions should not depend upon details. Details should depend upon abstractions. (抽象不可以相依於細節,而細節必須相依於抽象)
通常在做系統分析與設計時,當看系統的高度升高時,就能看到各元件的流程,再升高一點,就能看到系統之間的整合方式,再升高一點,就能看到整個系統的架構,所謂的高階 (High level),是指以系統分析的高度,將架構內各個具體的細節加以抽象化到較高的位置時,它就是高階物件,而當元件開始擺脫細節的時候,設計者就會抽絲剝繭將共同的特性抽出來,這時設計的位階就會逐步增加,當增加到不能再增加的時候,抽象就形成了。
在抽象的形成過程中,系統分析師基本上會檢視要抽象的成員抽出的程度,據以調整細節設計,因此細節會自然的相依於抽象,這些細節的部份的位階就會較低,因此也被稱為低階 (Low level) 物件,這類物件通常稱為具體類別 (Concrete Class)。
有興趣的讀者可以參考這篇文章,有更多的敘述。
看了一堆文字敘述,我想大家應該頭有點昏吧,這裡,Grant 也準備了一些例子。
public class ReportStatistic() {     
  private SQLAccess _access = new SQLAccess();          
  public double Sum() {         
  var items = _access.GetAllCost();         
    ...     
  } }  
  public class SQLAccess {     
  public List<CostItem> GetAllCost() {  
       ...     
  } 
}
在 ReportStatistic 方法中,我們可以看到整個系統的流程,代表著它就是高階模組,而我們也看到了 ReportStatstic 方法與負責實作 SQL 存取的細節方法 SQLAccess 方法有著緊密的相依性。
當今天系統規模一變大,很可能這種高耦合的狀況就會變得更加嚴重,一旦改了低階模組,連帶高階模組都必須要受到更動。
因此,在類別中,不應該直接使用另一個具有實作類別,而是要依賴抽象的介面,去承接繼承該介面的實作類別。它的目標就是解除物件與物件間,兩者的直接相依關係。
如果將上面的ReportStatistic方法改為依賴於抽象,就會變得好維護許多。
public class ReportStatistic()
{
   private IAccess _access = new SQLAccess();

   public double Sum()
   {
       var items = _access.GetAllCost();
       ...
   }
}

public interfalce IAccess
{
   List<CostItem GetAllCost();
}

public class SQLAccess : IAccess
{
   public List<CostItem> GetAllCost()
   {
       ...
   }
}

穩定依賴原則(SDP)

穩定依賴原則的意義在於我們要讓依賴關係朝著穩定的方向進行依賴。
之前提及的共同封閉原則中,我們提及某些元件被設計成是可變的,某些則是必須要被封閉的,只因為沒有 100% 的封閉,但我們不想要在上change的同時,所有的元件都會受到影響。
因此,對於任何元件而言,如果預期它是可變的,就不應該也不要讓一個難以更改的元件依賴它。

元件穩定性
不過,我們要如何去評估一個元件是不是穩定的呢?
一個可行的方法是「讓許多元件依賴它」,一個穩定的元件是相對不容易被改變的,同時也必須承擔被依賴的責任。
針對下圖中的 X,我們便可以稱其為穩定的元件,因為它對三個元件具有責任,而 X 本身不依賴任何元件,也代表著任何外部因素都不會讓其改變。
同樣的,我們也來看看甚麼是不穩定的元件。 下圖中的 Y 依賴三個外部元件,且本身不被任何元件依賴,不用負擔任何責任。

計算元件穩定性的度量
  • Fan-In: 輸入依賴度,代表說多少外部元件依賴此元件內部的類別
  • Fan-Out: 輸出依賴度,代表多少此元件內部的類別依賴於別人
  • Instability: 不穩定性 I = (FO)/(FI+FO) ,介於[0,1],0代表非常穩定 1代表非常不穩定,代表沒有任何元件依賴於該元件
計算元件穩定性的方式也很簡單,下圖中的Cc的穩定性為 1/(1+3) = 1/4。
不過,實際在設計系統時,我們不會希望所有的元件都是穩定的,我們希望有些元件是不穩定、有些是穩定的,而穩定與不穩定的元件的配額就必須要參照實際情況來決定了。

穩定抽象原則(SAP)

穩定抽象原則秉持著「一個穩定的元件也應該是抽象的」,這樣它的穩定性就不會影響它擴展。
會這樣是因為雖然我們不希望穩定模組經常被改變,不過還是有些特殊情形,我們希望穩定元件可以以穩定的方式進行擴展。
SAP 規定穩定性意味著「抽象性」,因此,依賴應朝向「抽象」的方向進行。
若定義了不穩定性I 以及抽象性A(Na/Nc) ,我們可以建立一個以I為橫軸 A為縱軸的座標圖:
Na(抽象類別及介面的總數)
Nc(類別總數)
最穩定的地方是座標圖的左上,最不穩定的地方是右下,但要注意的是,並不是所有元件都只能落在這兩個地方,因為元件的抽象性跟依賴性有程度之分,例如,抽象類別也可能依賴於其他的抽象類別。
但即便如此,還是有一些是元件不應該在的地方,分別是右上角的無用地帶,以及左下角的痛苦地帶。

無用地帶
若落於這個地方,代表元件有著最大的抽象,但卻沒有任何人依賴它,若無來依賴於它,那根本毫無用武之地。

痛苦地帶
在這裡的元件是具有高度穩定且具體的元件,我們不想要這種元件,因為其太具體所以很難進行擴展。
因此,盡可能避免落於無用以及痛苦地帶後,元件便會落於主序列上(Main Sequence)。
在主序列上的元件既不是太穩定、也並非太抽象,而最佳的端點則落於主序列的兩個頂點上,但這終究是不太可能達成的,落於主序列上的元件已經算是很不錯了。
最後,我們也許也有可能會希望微調目前系統上的元件,這個時候就必須要評估元件到主序列的距離,然後對其進行調整。

總結

這章的內容對於軟體及系統架構的設計非常有幫助,我們可以透過這些實際度量的準則設計未來的系統或評估目前的系統,看到這些內容,Grant也不禁恍然大悟,有時候在工作上,較資深的工程師會提點我們要小心依賴的方向,以及嚴格控管依賴,Grant 之前也許在不經意之中,讓系統形成了依賴環。
因此,在撰寫程式碼或設計系統前,也許考慮的多一點,就可以讓日後的維護變得相對輕鬆一點。
本文同步發表於格蘭特的部落格
參考資料
分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.