nest後端開發實戰(一)——依賴注入

前言

js單線程和無阻塞io讓它在處理高併發時有着得天獨厚的優勢,node應運而生,從此js進入到後端開發的行列。但是目前js在後端開發領域,並沒有得到廣泛和深度的應用。原因可能有這幾點:

  1. 異步代碼非常難看,回調地域。
  2. 沒有類型系統,ide不友好,不利於大規模應用的開發和維護。
  3. 缺乏相對標準的開發範式和開發框架。

其中第一點,目前async await已經非常成熟,不成構成問題;對於第二點,如果引入ts也將不是問題。ts完全兼容js,有類型系統又不失靈活,設計優雅適合大規模程序開發;至於第三點,有很多框架正在試圖解決該問題,比如egg、sails以及本文要討論的nest。

egg和sails沒有深度使用,只是有所關注,沒有太多發言權。他們解決的問題差不多,只是感覺實現方式有點“硬”,不夠自然。或許是因爲個人早前有java的經歷,所以更適應nest這套。java幾乎是後端開發的標準,把那套實踐了多年的理念借鑑過來,或許能夠解決很多問題。再結合js的靈活性以和性能優勢,說不定也是輕量級後端開發的一個好選擇。接下來,筆者準備寫一系列的文章來介紹nest後端開發的實踐,歡迎關注。

後端開發和依賴注入

我先嚐試着把依賴注入解釋清楚,這是nest的核心,所以從這裏開始。

前端開發和後端開發其實很不一樣。前端開發比較零碎,ui、交互、部分邏輯,而後端主要專注於邏輯。所以後端開發非常需要一種編程範式,以支持複雜的領域模型和業務邏輯管理。目前實踐得比較成熟的是面向對象的思想,而對於前端開發,面向對象的訴求其實並不大。

有了面向對象這個前提後,對象的依賴、創建、生命週期管理等就成了一個問題,依賴注入(DI)正是提供了一種標準方式來解決此問題。它將依賴的創建和銷燬交給“容器”去管理,使用者只管用,不操心具體細節。這也是控制反轉(IOC)思想的一種實現。

上面這段話說得比較抽象,現實一點,個人覺得它比較方便的解決了兩類問題:

  1. 上下文相關的依賴注入。就是需要根具不同的上下文注入不同的實例,共享上下文的狀態。
  2. 異步依賴的注入。

下面通過兩個例子來解釋。
例一,解釋上下文相關依賴問題。先看代碼:

class OrderDao {
    ...
}

class OrderService {
    private orderDao: OrderDao;
    constructor() {
        // 依賴OrderDao
        this.orderDao = new OrderDao();
    }
    ...
}

OrderService依賴OrderDao,並且在構造函數中實例化了依賴對象。這是一種強依賴關係,如果想在不同的上下文改變orderDao的實例就比較麻煩了。實際編程中可能存在類似場景,比如,跑測試用例的時候,想把dao換成mock的實現。

要達到上面的目的,代碼得先重構一下:

interface IOrderDao {
    ...
}

class OrderDaoImpl implements IOrderDao {
    ...
}

class OrderDaoMockImpl implements IOrderDao {
    ...
}

class OrderService {
    private orderDao: IOrderDao;
    // 依賴接口而不是實例
    constructor(orderDao: IOrderDao) {
        this.orderDao = orderDao;
    }
    ...
}

上面的代碼只是一種設計模式,和依賴注入無關。這種模式的思想是面向接口編程,而不是具體實現,從而達到解耦的目的。如果有依賴注入的容器,那麼只需簡單配置,容器會幫你管理依賴的創建和生命週期。具體的配置後面會講到。

例二,解釋異步依賴問題。假設OrderDao依賴mongo訪問數據庫,但是mongo client的創建卻是異步的。同時我們還希望mongo client是單例,因爲不希望頻繁的創建數據庫連接。下面是無依賴注入情況下的一種可能實現:

// 連接數據庫的示例代碼
const MongoClient = require('mongodb').MongoClient;
const url = 'mongodb://localhost:27017';
const dbName = 'myproject';
MongoClient.connect(url, function(err, client) {
 // 在這裏才能拿到client操作數據庫
  const db = client.db(dbName);
    // ...
});

class OrderDao {
    private mongo;
    constructor() {
        //異步的方式拿到mongo client
    }
}

能解決問題,只是代碼會難看一點。由於是異步,還可能存在使用OrderDao的時候,mongo並沒有連接好,此時調用會出錯。如果有依賴注入,就能比較優雅的處理此類問題。

以上說到的兩類場景,實際編程遇到的可能並不多,可能10%都不到,但是一旦遇上又非常難受。使用依賴注入,能夠優雅的解決上面的問題,同時代碼也更加規範。但使用依賴注入也是有一點點成本的,需要寫一點點的樣板代碼。依賴注入還具備傳染性,就是某個對象使用了依賴注入,依賴它的對象也必須使用,否則就亂套了。個人的看法是,首先還是保持簡潔,對象儘量設計成上下文無關或無狀態,只是在核心層(controller service, dao)使用依賴注入。

在nest中使用依賴注入

前面寫了這麼多,現在看下怎麼在nest中寫依賴注入。樣板代碼很簡單,大致是這樣:
1、依賴方通過@Injectable()修飾,告訴容器,“我是需要注入的”,同時在構造函數中聲明依賴。實例化時,依賴對象將通過構造函數注入。

// order.service.ts
@Injectable()
export class OrderService {
    // 注意這裏是個簡寫,等價於在OrderService下面定義了orderDao字段,同時在構造函數中給與賦值
    constructor(private readony orderDao: OrderDao) {}
}

2、定義providor,服務提供者。nest中有三種providor:class、value、factory。class providor就是普通的class,會被實例化後注入給依賴方;value providor可以是任意類型的值,直接注入給依賴方;factory providor是一個工廠方法,容器將先執行該方法,然後將返回值注入給依賴方,factory支持支持異步方法。

3、配置依賴關係。nest中有module的概念,主要用於描述在該scope下,具體的依賴和輸出關係。下面的代碼展示了三種providor的配置。

import {OrderDao} from './order.dao';// class providor

const classProvidor = { // 這也是class providor,和👆效果一樣
    provide: OrderDao,
    useClass: OrderDao
}

const valueProvidor = { // value providor
    provide: 'Config',
    useValue: process.env.NODE_ENV === 'prod' ? {...} : {...}
}
const factoryProvidor = { // factory provoidr
    provide: 'Mongo',
    useFactory: async () => {
        const client await MongoClient.connect(...);
        return client.db(dbName);
    }
}
@Module({
    providers: [OrderDao, valueProvidor, factoryProvidor] // 塞到這裏
})
export class OrderModule {}

4、依賴關係的解析。除了全局module(通過@Global()修飾即可成爲全局module),其它module都是一個單獨的scope。容器在創建對象時,會在當前scope和全局scope查找依賴。在決定具體使用哪個依賴時,會通過類型匹配或者具名的方式查找。兩種使用方式都很簡單,代碼如下:

class OrderService {
    constructor(
        readonly orderDao: OrderDao, // class匹配,通過在scope內搜索同類型class的providor
        @Inject('Config') config,// 具名匹配,通過在scope內搜索該名字的provoid
    ) {}
}

剩下的就交給容器幫你創建和管理對象了。

結語

開篇寫得比較簡單,主要是關於爲什麼要依賴注入的思考。接下來可能會逐步分享後端實踐方面的一些東西,比如項目結構,TS,微服務等具體問題的解決方案。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章