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

crossea發表於2019-02-16

前言

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

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

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

egg和sails沒有深度使用,只是有所關注,沒有太多發言權。他們解決的問題差不多,只是感覺實現方式有點“硬”,不夠自然。或許是因為個人早前有java的經歷,所以更適應nest這套。java幾乎是後端開發的標準,nest把那套實踐了多年的理念借鑑過來,或許能夠有些奇妙的化學反應。再結合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
    ) {}
}

剩下的就交給容器幫你建立和管理物件了。

結語

開篇寫得比較簡單,主要是關於為什麼要依賴注入的思考。接下來可能會逐步分享實踐方面的一些東西,比如專案結構,分層,基礎設施等具體問題的解決方案,歡迎關注。

相關文章