TypeScript 中的依賴注入

TypeScript 中的依賴注入

image.png

簡介

每個軟件程序都有其最基礎的構建模塊。在面向對象的編程語言中, 咱們使用類去構建複雜的體系架構。像建一幢大樓,咱們把模塊之間創建的聯繫稱之爲依賴。其餘的類爲了支持咱們類的需求,提供複雜的封裝操做。前端

一個類可能有引用其餘類的字段。所以,咱們不得不問及這些問題:這些引用是怎麼被建立的?是咱們去組合這些對象,仍是其餘類負責實例化它們?若是要實例化的類太複雜,而且咱們想避免出現垃圾代碼?全部這些問題均可以試圖經過依賴注入原則來解決。node

在開始示例以前,咱們必需要去理解關於依賴注入的一些相關概念。依賴注入原則告訴咱們,一個類應該去接收而非實例化它的依賴。經過委託方式來進行對象初始化,這能夠處理較爲複雜的操做,從而減小在類設計上的壓力。你能夠移除代碼中複雜的模塊,並經過其餘方式從新引入依賴。如何處理移除從新引入依賴,這是依賴管理的問題。你能夠手動處理全部對象的初始化和注入,可是這將會使整個系統變得複雜,咱們要儘可能避免這種狀況的發生。相反,你能夠將構造的職責轉移到 IoC 容器android

控制反轉是經過反轉整個程序的流程,以便容器對全部程序中涉及的依賴進行管理。你能夠建立一個容器,整個容器負責構造對象。當一個類須要實例化對象時,IoC 容器能夠提供它所須要的依賴。ios

IoC 提供了一種方法而非具體的實現。爲了使用依賴注入原則,你須要一個依賴注入框架。舉例以下:git

  • SpringDaggerJava 的依賴注入框架
  • HiltKotlin 的依賴注入框架
  • UnityC# 的依賴注入框架
  • InversifyNest.jsTypeDITypeScript 的依賴注入框架

概覽和角色劃分

image.png

在依賴注入原則中,咱們須要理解種不一樣的角色:github

  • 客戶端
  • 服務端
  • 接口
  • 注入器

服務端是咱們對外暴露服務使用的。這些類由 IoC 容器實例化和使用。一個客戶端經過 IoC 容器來使用這些服務。客戶端不該該被具體細節所困擾,所以接口須要確保客戶端和服務端保持協調。客戶端請求所需的依賴,注入器提供實例化服務。typescript

依賴注入的類型

當咱們討論如何在類中注入依賴時,能夠經過三種不一樣方式來實現:數據庫

  • 咱們能夠經過屬性(字段)提供依賴關係。在類上定義屬性,而後將具體的對象注入到該屬性中,這就是屬性注入。經過對外暴露這個屬性,但這樣作違背了面向對象程序設計的封裝原則;所以,要儘可能避免這種注入。
  • 咱們能夠經過方法提供依賴關係。對象的狀態應該是私有的,當外部想要改變該狀態時,它應該調用類的 getter/setter 方法。因此當你使用 setter 方法初始化類中的私有字段時,你可使用方法注入
  • 咱們能夠經過構造函數提供依賴關係。構造函數方法由於其基本屬性和對象構造高度融合在一塊兒。咱們一般支持經過構造函數進行注入,由於咱們的目標和構造函數的方法很相似。

使用 TypeDI 庫

一旦咱們理解了依賴注入的基本原理,使用什麼框架或者庫差異並不大。這篇文章裏,我選擇了 TypeScript 語言和 TypeDI 庫來展現這些基本概念。express

初始化 Yarn 和添加 TypeScript 會花點時間。我不想使用有名氣但沒有足夠註釋的項目配置,由於這會讓你感到無趣。因此我將給出初步的代碼並作簡要介紹。你能夠從這個 Github 倉庫查看和下載代碼。編程

任何 TypeScript 項目均可以做爲依賴注入演示的例子。但這篇文章裏我選擇了一個 Node/Express 應用做爲示例。我假設使用 TypeScript 的開發者要麼直接使用 Node/Express 服務器,要麼對它們有所瞭解。

當你查看 package.json 文件時,你能夠看到這些依賴項配置,讓我簡要介紹下它們:

  • express:Express 是編寫 Node.js RESTful 服務的流行框架。
  • reflect-metadata:一個用於元數據反射 API 的庫。它容許其餘庫經過裝飾器使用元數據。
  • ts-node:Node.js 沒法運行 TypeScript 文件。在代碼運行前,須要將 TypeScript 編譯爲 JavaScript。ts-node 幫你處理了這個過程。
  • typedi:TypeDI 是一個 TypeScript 依賴注入庫。咱們很快會看到它的示例。
  • typescript:咱們在這個項目中使用了 TypeScript,所以須要將它也做爲一個依賴。
  • @types/express:Express 庫的類型定義。
  • @types/node:Node.js 的類型定義。
  • ts-node-dev:這個庫容許你運行 TypeScript,並觀察某些文件的變化狀況。

你須要留意一些重要的編譯器選項配置。若是你看下 tsconfig.json,你能夠看到編譯過程的配置選項:

  • 咱們爲 reflect-metadatanode 指定了類型。
  • 咱們必須將 mitDecoratorMetadataexperimentalDecorators 設置爲 true。

全部的源碼都在 src 文件夾下。src/index.ts 是咱們項目的入口文件。這個文件包含了服務器的全部引導步驟:

import 'reflect-metadata';

import express from 'express';
import Container from 'typedi';
import UserController from './controllers/UserController';

const main = async () => {
  const app = express();

  const userController = Container.get(UserController);

  app.get('/users', (req, res) => userController.getAllUsers(req, res));

  app.listen(3000, () => {
    console.log('Server started');
  });
}

main().catch(err => {
  console.error(err);
});
複製代碼

這段代碼是一個只有一個端口的小型 Express 服務器。當你向 /users 路由發送一個 GET 請求時,它會返回一個用戶列表。main 函數的核心是 Container.get 方法。注意咱們並無使用 new 關鍵字或者實例化對象。咱們只是調用 IoC 容器返回的一個 UserController 實例方法。而後綁定了路由和控制器方法。

咱們的應用程序是一個虛擬的 RESTful 服務器,但我不想讓它沒有一點意義。我添加了四個不一樣的文件夾表明一個完備後端服務的基本部分。它們是 controllersmodelsrepositoriesservices。如今讓我一個個介紹下它們:

  • Controllers 文件夾包含咱們的 REST 控制器。它們負責協調客戶端和服務器之間的通訊。它們接收請求並返回響應。
  • Models 文件夾包含咱們的數據庫實體類。咱們沒有數據庫鏈接,也不須要,但創建一個合適的項目結構對於學習該項目有很大的幫助。咱們假設它是真是的數據庫實體並繼續咱們的項目。
  • Services 文件夾包含咱們的服務。它們經過訪問不一樣的存儲庫,負責爲 REST 控制器提供所需服務。
  • Repositories 文件夾包含咱們數據庫鏈接類。咱們使用 Data Mapper 模式來執行數據庫操做。該模式中咱們使用實體類來訪問數據庫並進行相關操做。

咱們不會把全部的東西都放到一個類中。請求和響應之間還有不少層級。這就是所謂的分層架構。經過類之間的依賴共享,咱們能夠更容易地進行依賴注入。

import { Request, Response } from "express";
import { Service } from "typedi";
import UserService from "../services/UserService";

@Service()
class UserController {
  constructor(private readonly userService: UserService) { }
  async getAllUsers(_req: Request, res: Response) {
    const result = await this.userService.getAllUsers();
    return res.json(result);
  }
}

export default UserController;
複製代碼

UserController 只有一個方法。getAllUsers 方法負責從用戶服務中獲取結果並進行傳輸。咱們給 UserController 添加類一個 Service 裝飾器,由於咱們但願這個類由 IoC 容器進行管理。在構造函數方法內部,咱們能夠看到這個類須要一個 UserService 實例。一樣,咱們不須要控制這個依賴關係。由於 TypeDI 容器爲 UserService 建立了一個實例,當它生成 UserController 實例時,它將注入到 UserService 中。

import { Service } from "typedi";
import User from "../models/User";
import UserRepository from "../repositories/UserRepository";

@Service()
class UserService {
  constructor(private readonly userRepository: UserRepository) { }
  async getAllUsers(): Promise<User[]> {
    const result = await this.userRepository.getAllUsers();
    return result;
  }
}

export default UserService;
複製代碼

UserService 和 UserController 很相似。咱們向類添加一個 Service 裝飾器,並在構造函數方法中指定它們想要的依賴項。

import { Service } from "typedi";
import User from "../models/User";

@Service()
class UserRepository {
  private readonly users: User[] = [
    { name: 'Emily' },
    { name: 'John' },
    { name: 'Jane' },
  ];

  async getAllUsers(): Promise<User[]> {
    return this.users;
  }
}

export default UserRepository;
複製代碼

UserRepository 是咱們的最後一步。咱們用 Service 來註解這個類,可是咱們沒有任何依賴關係。由於沒有數據庫鏈接,因此我只是將已硬編碼過的用戶列表做爲私有屬性添加到類中。

結論

依賴注入是管理複雜對象初始化的有力工具。手動進行依賴注入總比什麼都不作要好,可是使用 TypeDI 更簡單可行。當你要開始作一個新項目時,你應該明確地考慮下依賴注入原則並給予適當嘗試。

你能夠在這個 GitHub 分支找到本文的代碼。

你能夠在 GitHubLinkedInTwitter 找到我。

感謝閱讀,祝你快樂。

引用

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索