上一篇文章中,我們提到 SRP,然後接下來我不依順序來介紹以下兩個,因為這兩個和 SRP 我自己覺得概念上事實上都有涵蓋到。
不應該強迫用戶端依賴它們不需要的介面。
我們用產品來當範例。
就是很明顯 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...");
}
}
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 ) 的定義如下 :
- 高層次的模組不應該依賴於低層次的模組,兩者都應該依賴於抽象介面。
- 抽象不要依賴細節,細節要依賴於抽象
下面是一段 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 的範例。
我們做了幾件事情來調整程式碼 :
為什麼將 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 的定義來比對看看 :
- 高層次的模組不應該依賴於低層次的模組,兩者都應該依賴於抽象介面
從下面可以看到 courseService (高層次) 沒有依賴 courseRepository(低層次),而是依賴於 ICourseRepository(抽象介面)
2.抽象不要依賴細節,細節要依賴於抽象
ICourseRepository 在設計時,沒有依賴細節,例如叫 IMongoDBCourseRepository,而細節有符合抽象實作。
反轉在那?
你看看左邊與右邊的依賴是不是整個反過來了?
SRP 它的概念為 :
每個元件都應該僅有一種且唯一種被
修改的理由
然後這裡被修改的理由有 2 :
其中我自已覺得,這兩個原則 ISP 與 DIP 實際上也可以降低這兩個變動源。
首先是 ISP 強調不應該被迫實現不需要的接口方法,這點和 SRP 的兩個變動源都有關:
首先是第 1 個,如果外面的使用者使用了 Product 介面,然後他包山包海的,那是不是就代表,它就有可能讓包山包海的不同情境上使用,所以如果每個情境變了,那是不是有可能 Product 類別就也要變了 ?
接下來是第 2 個,如果你使用違反 ISP 的介面 ( 例如上面範例的 Product ),這樣就會依賴了很多你不需要的東西,然後它們又變動了,可能你就又需要修改了。以上面的範例來看,實體商品的是不是就有可能需要依賴download
所需要的元件 ? 但他際上不需要。
接下來談談 DIP,以上面 course 範例為例。原本 courseService 可能會因為 courseRepository 的變動而需要變動,那如果把變動源移至 domain layer 這塊來決定,就變成概念上是 courseService 變動,才會導致 courseRepository,這樣我們就移除了元件所依賴變動的問題。
根據這兩原則,我自已總結一下,我開發時可能會思考的東西 :
不過說起來簡單,但是實務上你在忙需求時,會不會思考到這些,就真的是看腦袋肌肉 + legacy code 囉,我自已在忙需求快死的時後,而且加上是改 legacy code 時,我自已腦袋停止這些思考了……
這是題外話,我自已覺得一間公司的可維護性,除了工程師本身能力以外,還有就是 feature team 的 process 有沒有包含重構流程,在需求天天進來的情況,deadline 又緊的情況 + legacy code 的情況下,我自已是 code review 這方面不太會抓太緊,我比較重視 be better,但問題就是 feature team 的 process 有沒有可以 be better 才是大問題。