首頁 >web前端 >js教程 >掌握依賴倒置原則:使用 DI 實現乾淨程式碼的最佳實踐

掌握依賴倒置原則:使用 DI 實現乾淨程式碼的最佳實踐

Linda Hamilton
Linda Hamilton原創
2024-11-30 00:25:11264瀏覽

如果您熟悉物件導向程式設計,或剛開始探索它,您可能遇到過縮寫SOLID。 SOLID 代表了一組旨在幫助開發人員編寫乾淨、可維護且可擴展的程式碼的原則。在這篇文章中,我們將重點放在 SOLID 中的“D”,它代表依賴倒置原則

但在深入了解細節之前,讓我們先花點時間了解這些原則背後的「原因」。

在物件導向程式設計中,我們通常將應用程式分解為類別,每個類別封裝特定的業務邏輯並與其他類別互動。例如,想像一個簡單的線上商店,用戶可以將產品添加到購物車中。此場景可以透過多個類別一起進行建模來管理商店的營運。讓我們以這個例子為基礎來探索依賴倒置原則如何改進我們系統的設計。

class ProductService {
 getProducts() {
   return ['product 1', 'product 2', 'product 3'];
 }
}


class OrderService {
 constructor() {
   this.productService = new ProductService();
 }

 getOrdersForUser() {
   return this.productService.getProducts();
 }
}


class UserService {
 constructor() {
   this.orderService = new OrderService();
 }

 getUserOrders() {
   return this.orderService.getOrdersForUser();
 }
}

如我們所見,像 OrderServiceProductService 這樣的依賴關係在類別建構子中緊密耦合。這種直接依賴使得替換或模擬這些元件變得困難,這在測試或交換實作時提出了挑戰。

依賴注入(DI)

依賴注入 (DI) 模式提供了這個問題的解決方案。透過遵循 DI 模式,我們可以解耦這些依賴關係,並使我們的程式碼更加靈活和可測試。以下是我們如何重構程式碼來實作 DI:

Mastering the Dependency Inversion Principle: Best Practices for Clean Code with DI

class ProductService {
 getProducts() {
   return ['product 1', 'product 2', 'product 3'];
 }
}


class OrderService {
 constructor(private productService: ProductService) {}

 getOrdersForUser() {
   return this.productService.getProducts();
 }
}


class UserService {
 constructor(private orderService: OrderService) {}

 getUserOrders() {
   return this.orderService.getOrdersForUser();
 }
}


new UserService(new OrderService(new ProductService()));

我們明確地將依賴項傳遞給每個服務的建構函數,這雖然是朝著正確方向邁出的一步,但仍然會導致緊密耦合的類別。這種方法確實稍微提高了靈活性,但它並沒有完全解決使我們的程式碼更加模組化且易於測試的根本問題。

依賴倒置原理(DiP)

依賴倒置原理(DiP)透過回答關鍵問題更進一步:我們應該傳遞什麼?這個原則表明,我們不應傳遞具體的實現,而應僅傳遞必要的抽象,特別是與預期介面匹配的依賴項。

例如,考慮帶有 getProducts 方法的 ProductService 類,該方法傳回 產品數組 。我們可以透過多種方式實現它,而不是直接將 ProductService 耦合到特定的實作(例如,從資料庫中取得資料)。一種實作可能從資料庫取得產品,而另一種實作可能傳回硬編碼的 JSON 物件以進行測試。關鍵是兩種實現共享相同的接口,確保靈活性和可互換性。

Mastering the Dependency Inversion Principle: Best Practices for Clean Code with DI

控制反轉 (IoC) 和服務定位器

為了將此原則付諸實踐,我們經常依賴一種稱為控制反轉 (IoC) 的模式。 IoC 是一種技術,將對依賴項的建立和管理的控制從類別本身轉移到外部元件。這通常是透過依賴注入容器或服務定位器來實現的,它充當註冊表,我們可以從中請求所需的依賴項。透過 IoC,我們可以動態地註入適當的依賴項,而無需將它們硬編碼到類別構造函數中,從而使系統更加模組化並且更易於維護。

Mastering the Dependency Inversion Principle: Best Practices for Clean Code with DI

class ProductService {
 getProducts() {
   return ['product 1', 'product 2', 'product 3'];
 }
}


class OrderService {
 constructor() {
   this.productService = new ProductService();
 }

 getOrdersForUser() {
   return this.productService.getProducts();
 }
}


class UserService {
 constructor() {
   this.orderService = new OrderService();
 }

 getUserOrders() {
   return this.orderService.getOrdersForUser();
 }
}

正如我們所看到的,依賴項是在容器內註冊的,這使得它們可以在必要時被替換或交換。這種靈活性是一個關鍵優勢,因為它促進了組件之間的鬆散耦合。

但是,這種方法有一些缺點。由於依賴項是在運行時解析的,因此如果出現問題(例如,如果依賴項遺失或不相容),可能會導致運行時錯誤。此外,無法保證註冊的依賴項將嚴格符合預期的接口,這可能會導致微妙的問題。這種依賴關係解析方法通常稱為服務定位器模式,並且在許多情況下被認為是反模式,因為它依賴執行時解析並且有可能掩蓋依賴關係。

InversifyJS

JavaScript 中用於實現 控制反轉 (IoC) 模式的最流行的庫之一是 InversifyJS。它提供了一個強大且靈活的框架,以乾淨、模組化的方式管理依賴關係。然而,InversifyJS 有一些缺點。一個主要限制是設定和管理依賴項所需的樣板程式碼量。此外,它通常需要以特定的方式建立應用程序,這可能不適合每個專案。

Mastering the Dependency Inversion Principle: Best Practices for Clean Code with DI

InversifyJS 的替代方案是Friendly-DI,這是一種輕量級且更簡化的方法,用於管理 JavaScript 和 TypeScript 應用程式中的依賴關係。它的靈感來自於 Angular 和 NestJS 等框架中的 DI 系統,但設計得更簡約、簡潔。

Friendly-DI 的一些主要優點包括:

  • 體積小:只有 2 KB,沒有外部依賴。
  • 跨平台:在瀏覽器和 Node.js 環境中無縫運作。
  • 簡單的 API:直覺且易於使用,只需最少的配置。
  • MIT 許可證:具有寬鬆許可的開源。

但是,需要注意的是,Friendly-DI 是專為 TypeScript 設計的,您需要先安裝其依賴項才能開始使用它。

class ProductService {
 getProducts() {
   return ['product 1', 'product 2', 'product 3'];
 }
}


class OrderService {
 constructor() {
   this.productService = new ProductService();
 }

 getOrdersForUser() {
   return this.productService.getProducts();
 }
}


class UserService {
 constructor() {
   this.orderService = new OrderService();
 }

 getUserOrders() {
   return this.orderService.getOrdersForUser();
 }
}

也擴充tsconfig.json:

class ProductService {
 getProducts() {
   return ['product 1', 'product 2', 'product 3'];
 }
}


class OrderService {
 constructor(private productService: ProductService) {}

 getOrdersForUser() {
   return this.productService.getProducts();
 }
}


class UserService {
 constructor(private orderService: OrderService) {}

 getUserOrders() {
   return this.orderService.getOrdersForUser();
 }
}


new UserService(new OrderService(new ProductService()));

上面的範例可以用Friendly-DI修改:

class ServiceLocator {
 static #modules = new Map();

 static get(moduleName: string) {
   return ServiceLocator.#modules.get(moduleName);
 }

 static set(moduleName: string, exp: never) {
   ServiceLocator.#modules.set(moduleName, exp);
 }
}

class ProductService {
 getProducts() {
   return ['product 1', 'product 2', 'product 3'];
 }
}


class OrderService {
 constructor() {
   const ProductService = ServiceLocator.get('ProductService');
   this.productService = new ProductService();
 }

 getOrdersForUser() {
   return this.productService.getProducts();
 }
}


class UserService {
 constructor() {
   const OrderService = ServiceLocator.get('OrderService');
   this.orderService = new OrderService();
 }

 getUserOrders() {
   return this.orderService.getOrdersForUser();
 }
}

ServiceLocator.set('ProductService', ProductService);
ServiceLocator.set('OrderService', OrderService);


new UserService();
  1. 正如我們所看到的,我們添加了 @Injectable() 裝飾器,它將我們的類別標記為可注入的,表明它們是依賴注入系統的一部分。這個裝飾器允許 DI 容器知道這些類別可以在需要的地方實例化和注入。

  2. 當在建構函式中將類別宣告為依賴項時,我們不會直接綁定到具體類別本身。相反,我們根據其介面來定義依賴關係。這將我們的程式碼與特定實作解耦,並提供更大的靈活性,從而在需要時更容易交換或模擬依賴項。

  3. 在此範例中,我們將 UserService 放置在 App 類別中。這種模式稱為組合根組合根是應用程式中組裝和注入所有依賴項的中心位置 - 本質上是我們應用程式依賴關係圖的「根」。透過將此邏輯保留在一個位置,我們可以更好地控制如何在整個應用程式中解析和注入依賴項。

最後一步是在 DI 容器中註冊 App 類,這將使容器能夠在應用程式啟動時管理生命週期和所有依賴項的注入。

Mastering the Dependency Inversion Principle: Best Practices for Clean Code with DI

npm i friendly-di reflect-metadata

如果我們需要替換應用程式中的任何類,我們只需要按照原始介面建立模擬類:

class ProductService {
 getProducts() {
   return ['product 1', 'product 2', 'product 3'];
 }
}


class OrderService {
 constructor() {
   this.productService = new ProductService();
 }

 getOrdersForUser() {
   return this.productService.getProducts();
 }
}


class UserService {
 constructor() {
   this.orderService = new OrderService();
 }

 getUserOrders() {
   return this.orderService.getOrdersForUser();
 }
}

然後使用替換方法,我們將可替換類別宣告為類比類別:

class ProductService {
 getProducts() {
   return ['product 1', 'product 2', 'product 3'];
 }
}


class OrderService {
 constructor(private productService: ProductService) {}

 getOrdersForUser() {
   return this.productService.getProducts();
 }
}


class UserService {
 constructor(private orderService: OrderService) {}

 getUserOrders() {
   return this.orderService.getOrdersForUser();
 }
}


new UserService(new OrderService(new ProductService()));

友善-DI我們可以多次替換:

class ServiceLocator {
 static #modules = new Map();

 static get(moduleName: string) {
   return ServiceLocator.#modules.get(moduleName);
 }

 static set(moduleName: string, exp: never) {
   ServiceLocator.#modules.set(moduleName, exp);
 }
}

class ProductService {
 getProducts() {
   return ['product 1', 'product 2', 'product 3'];
 }
}


class OrderService {
 constructor() {
   const ProductService = ServiceLocator.get('ProductService');
   this.productService = new ProductService();
 }

 getOrdersForUser() {
   return this.productService.getProducts();
 }
}


class UserService {
 constructor() {
   const OrderService = ServiceLocator.get('OrderService');
   this.orderService = new OrderService();
 }

 getUserOrders() {
   return this.orderService.getOrdersForUser();
 }
}

ServiceLocator.set('ProductService', ProductService);
ServiceLocator.set('OrderService', OrderService);


new UserService();

就這樣,如果您對此主題有任何意見或澄清,請在評論中寫下您的想法。

以上是掌握依賴倒置原則:使用 DI 實現乾淨程式碼的最佳實踐的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn