iT邦幫忙

2024 iThome 鐵人賽

DAY 5
0
Software Development

一個好的系統之好維護基本篇 ( 馬克版 )系列 第 5

Day-05: 耦合性三大原則 ADP、SDP、SAP

  • 分享至 

  • xImage
  •  

同步至 medium

今天主要來談談耦合性三大原則 :

  • 無循環依賴原則 (ADP:Acyclic Dependencies Principle)
  • 穩定依賴原則 (SDP:Stable-Dependencies Principle)
  • 穩定抽象原則 (SAP:Stable Abstractions Principle)

無循環依賴原則 ( ADP:Acyclic Dependencies Principle )

在元件的依賴圖中,不能有環

事實上就是我們常說的,不要有循環依賴,也就是如下的情況 :

  • A 依賴 B
  • B 依賴 A

這事實上在實務上非常的常見,例如在我們開發時,假設有兩個 service 分別為 userService 與 courseService,那這個時後如果有個需求是取得用戶有的課程數,而另一個需求是取得這個課程有多少學生,那這樣是不是就很容易產生了 ?

class UserService {
  signup(){
    console.log('sign up')
  }

  getUserCourses(userId: string): string[] {
    // UserService 依賴 CourseService
    return courseService.getCoursesByUser(userId);
  }
  
  getUsersByIds(userIds: string[]){
    return [UserA, userB, userC]
  }
}

class CourseService {
  
  publishCourse(){
    console.log('publish a course')
  }

  getCourseStudents(courseId: string){
    const studenetIds = [1,2,3]
    const students = userService.getUsersByIds(studenetIds);
    return students;
  }
  
  getCoursesByUser(userId: string){
     return [CourseA, CourseB]
  }
}

會發生什麼事情呢 ?

首先要先說一下,很多人會因為循環依賴導致載入有問題,然後就直接用 DI 就可以解決,那這樣是不是就符合 ADP 呢 ? 不是喔 ~ 它真正的目的是要 :

解耦,不然整個就是個大泥球

然後接下來簡單說一下,有幾個地方可能會發生問題 :

  • 系統會變的比較難維護,因為改動一個元件後,可能導致另一個元件,然後另一個元件又影響另一個元件,最後又影響回自已。
  • 有時後會讓系統出現不可預期。
  • 如果是以微服務為單位來看,你會很難做到獨立部署,因為環環相扣。

大部份整個循環依賴產生的原因都是想用啥就用啥,例如 userService 因為業務需求,要去抓 course 相關的東西,就很直覺得去 courseService。整體來說就是沒有考慮元件與元件的上下游關係。

解法

將相互依賴的元件進行拆分,並且讓元件有方向性。

最常見的,應該就是引入一個中介層來解決這件事 :

class UserService {
  signup(){
    console.log('sign up')
  }
  
  getUsersByIds(userIds: string[]){
    return [UserA, userB, userC]
  }
}

class CourseService {
  
  publishCourse(){
    console.log('publish a course')
  }
}

class UserCourseService {
  getUserCourses(userId: string): string[] {
    return this.getCoursesByUser(userId);
  }

  getCourseStudents(courseId: string){
    const studenetIds = [1,2,3]
    const students = userService.getUsersByIds(studenetIds);
    return students;
  }
  
  private getCoursesByUser(userId: string){
     return [CourseA, CourseB]
  }
}

然後事實上看下面的圖就很清楚他們的關係,從循環變成有方向性的依賴,然後事實上也讓職責有點模糊的,變的比較好一些 :

原本:

  • UserService: 處理所有 user 相關的事情。
  • CourseService: 處理所有 course 相關的事情。

變成:

  • UserService: 處理註冊與用戶基本資訊的職責。
  • CourseService: 處理課程開課的職責。
  • UserCourseService: 處理課程與學生相關的職責。

雖然這樣拆應該還有很多問題,但事實上就是在解這個過程中,可以慢慢看出那些是放在一起的。

https://ithelp.ithome.com.tw/upload/images/20240919/20089358nIGmZJTepF.png

https://ithelp.ithome.com.tw/upload/images/20240919/200893581j0m7S47uz.png


穩定依賴原則 ( SDP:Stable-Dependencies Principle )

一個元件,應該只依賴比它更穩定的元件

那什麼是更穩呢?簡單的說就是元件不容易被修改,然後在《無瑕的程式碼 敏捷完整篇》,它用下面這張圖來表達穩定。

https://ithelp.ithome.com.tw/upload/images/20240919/2008935835N7VtZ6HH.png

我自已是覺得有點怪怪的,但也有可能我自已還沒有悟了。然後我覺得怪怪地方在於 :

一個元件如果給越多人用,某些方面來說是好事,但相對來說你為了別人而改變的機率也變大了,infra 層還合理,但 domain 層就會讓我覺得有點危險。

所以事實上我自已比較傾向根據 SRP 原則的那張圖來看再加上變動頻率來一起看。

先說一下變動頻率,假設我們現在是 logger 這種類型的元件,事實上我們通常做完後,應該也幾乎不太會動它,所以整體來說根據下面那張圖加上這個,整理如下三點。

https://ithelp.ithome.com.tw/upload/images/20240919/200893580SwutpkzdP.png

  • 元件依賴其它元件的數量很少。
  • 元件被很少的情境或角色使用。
  • 元件很少被變動 (就是沒需求),例如 logger、infra 之類的元件。

然後為啥要依賴比它更穩定的元件呢 ?

假設我們現在有 A 元件 (穩定) 與 B 元件 (不穩定),然後現在的依賴方向如下 :

A → B(不穩定)

那結果會發生什麼事呢 ? 那就是每一次 B 的修改都可能會影響 A,而且因為 B 不穩定,所以這也代表可能你每個星期都會因為 B 的變動,而需要改 A。

這樣 A 的維護者一定會打死 B,但實際上是依賴方向錯了,是 A 自已找死。所以應該是改成

B -> A(穩定)


穩定抽象原則 ( SAP:Stable Abstractions Principle )

《Clean Architecture》書中認為 :

  1. 一個穩定的元件應該也是抽象的
  2. 一個不穩定的元件應該是具體的

穩定的範例

首先我們來說說穩定的範例,以 logger 來說說,它有以下幾個特點 :

  • 正常來說我們幹完,應該就不太會動。
  • 基本上雖然呼叫的地方很多,但是使用的 context 大部份只有一個,就是寫 log。

所以通常它會這樣設計,這樣的好處就是我們裡面如何修改,外面基本上幾乎完全不會影響到。

interface ILogger {
  log(message: string): void;
  error(message: string): void;
}
class ConsoleLogger implements ILogger {
  log(message: string): void {
    console.log(`[LOG]: ${message}`);
  }

  error(message: string): void {
    console.error(`[ERROR]: ${message}`);
  }
}
class FileLogger implements ILogger {
  log(message: string): void {
    // 假設這裡是寫入文件的實現
    console.log(`[FILE LOG]: ${message}`);
  }

  error(message: string): void {
    // 假設這裡是寫入文件的錯誤實現
    console.log(`[FILE ERROR]: ${message}`);
  }
}

不穩定的範例

接下來,說說不穩定應該的元件應該是具體的範例,我們以 notficationService 來當範例。我們現在的程式碼如下,然後主要幾個點在於 :

  • 我們有一個抽象 INotificationService,然後不知道裡面是用什麼通知。
  • 然後有 2 具體實現 email 與 sms。

問題出在那呢 ?

那就是我們實際上需求變動時,是很容易動到 sendNotification 要帶入的東西,例如我們信件說不定要帶寄件者,然後 sms 可能還需要帶電話號碼之類的。

所以他這裡反而建議不要抽象,反而具體點比較好。

interface INotificationService {
  sendNotification(userId: string, message: string): void;
}
class EmailNotificationService implements INotificationService {
  sendNotification(userId: string, message: string): void {
    console.log(`Sending email to user ${userId}: ${message}`);
  }
}

class SmsNotificationService implements INotificationService {
  sendNotification(userId: string, message: string): void {
    console.log(`Sending SMS to user ${userId}: ${message}`);
  }
}

小結

簡單用一句話來整理一下今天的重點。

  • ADP:防止元件之間形成循環依賴,保持依賴關係的單向性。
  • SDP:不穩定的元件應依賴於更穩定的模組,確保依賴關係向穩定性更高的方向發展。
  • SAP:穩定的元件應該是抽象的,而不穩定的模組應該是具體的,這樣可以保持系統的穩定性和靈活性。

根據這三個原則,應該可以設計出更低耦合的服務,大概啦… 我自已感覺有些東西都還有沒有悟。


上一篇
Day-04: 設計原則 SOLID - OCP、LSP
下一篇
Day 06 : 聚合性三大原則 - REP、CCP、CRP
系列文
一個好的系統之好維護基本篇 ( 馬克版 )30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言