iT邦幫忙

2024 iThome 鐵人賽

DAY 3
0
Software Development

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

Day-03 : 設計原則之 SOLID - ISP、DIP

  • 分享至 

  • xImage
  •  

同步至 medium

上一篇文章中,我們提到 SRP,然後接下來我不依順序來介紹以下兩個,因為這兩個和 SRP 我自己覺得概念上事實上都有涵蓋到。

  • 介面隔離原則 (ISP: Interface Segregation Principle)
  • 依賴反轉原則 (DIP: Dependency Inversion Principle)

介面隔離原則 ( ISP: Interface Segregation Principle )

不應該強迫用戶端依賴它們不需要的介面。

我們用產品來當範例。

就是很明顯 product 提供 4 個行為,但結果實際上在實體商品 (PhysicalBook) 上,只用了 getPrice 與 ship,而在虛擬商品上(VirtualCourse)只用了 download、subscribe、getPrice。

所以是不是它們分別都有不需要的 interface 行為 ?

interface Product {
  getPrice(): number;
  ship(): void;
  download(): void;
  subscribe(): void;
}


class PhysicalBook implements Product {
  getPrice(): number {
    return 20;
  }

  ship(): void {
    console.log("Shipping the book...");
  }

  download(): void {
    throw new Error("PhysicalBook cannot be downloaded.");
  }

  subscribe(): void {
    throw new Error("PhysicalBook does not support subscriptions.");
  }
}

class VirtualCourse implements Product {
  private price: number;

  constructor(price: number) {
    this.price = price;
  }

  getPrice(): number {
    return this.price;
  }

  // 虛擬課程不需要運送
  ship(): void {
    throw new Error("VirtualCourse cannot be shipped.");
  }

  // 虛擬課程需要下載資料
  download(): void {
    console.log("Downloading the course materials...");
  }

  // 假設虛擬課程支持訂閱
  subscribe(): void {
    console.log("Subscribing to the course...");
  }
}

符合 ISP 的程式碼

interface Product {
  getPrice(): number;
}

interface Downloadable {
  download(): void;
}

interface Subscribable {
  subscribe(): void;
}

class VirtualCourse implements Product, Downloadable, Subscribable {
  private price: number;

  constructor(price: number) {
    this.price = price;
  }

  getPrice(): number {
    return this.price;
  }

  download(): void {
    console.log("Downloading the course materials...");
  }

  subscribe(): void {
    console.log("Subscribing to the course...");
  }
}

依賴反轉原則 ( DIP: Dependency Inversion Principle )

DIP ( Dependency Inversion Principle ) 的定義如下 :

  1. 高層次的模組不應該依賴於低層次的模組,兩者都應該依賴於抽象介面。
  2. 抽象不要依賴細節,細節要依賴於抽象

下面是一段 courseService 與 courseRepository 的範例。首先我們先來看一段違反 DIP 的情況如下程式碼如下,根據 code 可以知道這裡變動的理由就是 :

當 courseRepository 被修改時,例如變動欄位或是修改參數時,那這樣 courseService 是不是也要修改呢 ?

~備註~
高層次與低層次的差異在於,高層次是要完成我們的業務行為,例如訂單付款行為,然後接下來低層次就是實作的層級,例如呼叫第三方金流,然後儲到資料庫中之類的。

然後在《Clean Architecture》在的說法為,離 I/O 越遠的層級越高,反之則越低,仔細想想是不是和上面的概念很接近 ?

// Domain Layer - CourseService.ts (違反 DIP)
import { CourseRepository } from '../infrastructure/CourseRepository';

class CourseService {
    private courseRepository: CourseRepository;

    constructor() {
        // 直接依賴具體的 CourseRepository 而非抽象接口
        this.courseRepository = new CourseRepository();
    }

    async getCourse(courseId: string): Promise<Course | null> {
        return await this.courseRepository.findCourseById(courseId);
    }

    async createCourse(course: Course): Promise<void> {
        await this.courseRepository.saveCourse(course);
    }
}

然後接下來我們看看符合 DIP 的範例。

我們做了幾件事情來調整程式碼 :

  1. 建立 ICourseRepository Interface,並且將它放在 Domain Layer
  2. 然後 CourseRepository 實作它。
  3. 然後 CourseService 依賴它。

為什麼將 interface 放在 domain layer 呢 ?

因為要讓變的方向,從高層到低層,而不是低層會影響高層。也就是說 db 變動不會影響到業務層的東西。

這就是所謂的依賴反轉

// Domain Layer -  ICourseRepository.ts
interface ICourseRepository {
    findCourseById(courseId: string): Promise<Course | null>;
    saveCourse(course: Course): Promise<void>;
}

------------------------------------------------------------------

// Domain Layer -  CourseService.ts
import { ICourseRepository } from './ICourseRepository';
import { Course } from './Course';

class CourseService {
    private courseRepository: ICourseRepository;

    constructor(courseRepository: ICourseRepository) {
        this.courseRepository = courseRepository;
    }

    async getCourse(courseId: string): Promise<Course | null> {
        return await this.courseRepository.findCourseById(courseId);
    }

    async createCourse(course: Course): Promise<void> {
        await this.courseRepository.saveCourse(course);
    }
}

// Infra Layer - CourseRepository.ts

export class CourseRepository implements ICourseRepository {
    async findCourseById(courseId: string): Promise<Course | null> {
        // 模擬從資料庫查詢數據
        return await mockDatabase.find(courseId); 
    }

    async saveCourse(course: Course): Promise<void> {
        // 模擬將課程保存到資料庫
        await mockDatabase.save(course);
    }
}

下圖為我們的關係圖,我們直接拿 DIP 的定義來比對看看 :

  1. 高層次的模組不應該依賴於低層次的模組,兩者都應該依賴於抽象介面

從下面可以看到 courseService (高層次) 沒有依賴 courseRepository(低層次),而是依賴於 ICourseRepository(抽象介面)

2.抽象不要依賴細節,細節要依賴於抽象

ICourseRepository 在設計時,沒有依賴細節,例如叫 IMongoDBCourseRepository,而細節有符合抽象實作。

反轉在那?

你看看左邊與右邊的依賴是不是整個反過來了?

https://ithelp.ithome.com.tw/upload/images/20240917/20089358uOLlRmJTC3.png


談談 SRP 與 ISP + DIP 的關連

SRP 它的概念為 :

每個元件都應該僅有一種且唯一種被修改的理由

然後這裡被修改的理由有 2 :

  • 使用的角色,或是所謂的使用的情境
  • 元件所依賴的東西

其中我自已覺得,這兩個原則 ISP 與 DIP 實際上也可以降低這兩個變動源。

https://ithelp.ithome.com.tw/upload/images/20240917/20089358Tkjd3Cmecv.png

首先是 ISP 強調不應該被迫實現不需要的接口方法,這點和 SRP 的兩個變動源都有關:

  1. 使用的角色,或是所謂的使用的情境
  2. 依賴的原件

首先是第 1 個,如果外面的使用者使用了 Product 介面,然後他包山包海的,那是不是就代表,它就有可能讓包山包海的不同情境上使用,所以如果每個情境變了,那是不是有可能 Product 類別就也要變了 ?

接下來是第 2 個,如果你使用違反 ISP 的介面 ( 例如上面範例的 Product ),這樣就會依賴了很多你不需要的東西,然後它們又變動了,可能你就又需要修改了。以上面的範例來看,實體商品的是不是就有可能需要依賴download所需要的元件 ? 但他際上不需要。

接下來談談 DIP,以上面 course 範例為例。原本 courseService 可能會因為 courseRepository 的變動而需要變動,那如果把變動源移至 domain layer 這塊來決定,就變成概念上是 courseService 變動,才會導致 courseRepository,這樣我們就移除了元件所依賴變動的問題。


小結

根據這兩原則,我自已總結一下,我開發時可能會思考的東西 :

  1. 在設計 interface 時,有沒有那些實作的 class 實際上是不需要這個 interface 的某些實作呢 ?
  2. 看到很臃腫的 interface,可能要考慮一下排入重構。
  3. 只提供用戶需要的 inteface。
  4. 元件都依賴抽象。
  5. 要分的清楚那些是高層次,那些是低層次。
  6. 抽象設計時,不要和細節混在一起。
  7. inteface 要放那裡,要想清楚。

不過說起來簡單,但是實務上你在忙需求時,會不會思考到這些,就真的是看腦袋肌肉 + legacy code 囉,我自已在忙需求快死的時後,而且加上是改 legacy code 時,我自已腦袋停止這些思考了……

這是題外話,我自已覺得一間公司的可維護性,除了工程師本身能力以外,還有就是 feature team 的 process 有沒有包含重構流程,在需求天天進來的情況,deadline 又緊的情況 + legacy code 的情況下,我自已是 code review 這方面不太會抓太緊,我比較重視 be better,但問題就是 feature team 的 process 有沒有可以 be better 才是大問題。


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

尚未有邦友留言

立即登入留言