乐趣区

nestjs后端开发实战(一)——依赖注入

前言
js 单线程和无阻塞 io 让它在处理高并发时有着得天独厚的优势,node 应运而生,从此 js 进入到后端开发的行列。但是目前 js 在后端开发领域,并没有得到广泛和深度的应用。原因可能有这几点:

异步代码非常难看,回调地域。
没有类型系统,ide 不友好,不利于大规模应用的开发和维护。
缺乏相对标准的开发范式和开发框架。

其中第一点,目前 async await 已经非常成熟,不成构成问题;对于第二点,如果引入 ts 也将不是问题。ts 完全兼容 js,有类型系统又不失灵活,设计优雅适合大规模程序开发;至于第三点,有很多框架正在试图解决该问题,比如 egg、sails 以及本文要讨论的 nest。nest 是一个对标 spring 的后端开发框架,目前还年轻,但发展速度挺快。
egg 和 sails 没有深度使用,只是有所关注,没有太多发言权。他们解决的问题差不多,只是感觉实现方式有点“硬”,不够自然。或许是因为个人早前有 java 的经历,所以更适应 nest 这套。java 几乎是后端开发的标准,nest 把那套实践了多年的理念借鉴过来,或许能够有些奇妙的化学反应。再结合 js 的灵活性以和性能优势,说不定也是轻量级后端开发的一个好选择。接下来,笔者准备写一系列的文章来介绍 nest 后端开发的实践,欢迎关注。
后端开发和依赖注入
我先尝试着把依赖注入解释清楚,这是 nest 的核心,所以从这里开始。
前端开发和后端开发其实很不一样。前端开发比较零碎,ui、交互、部分逻辑,而后端主要专注于逻辑。所以后端开发非常需要一种编程范式,以支持复杂的领域模型和业务逻辑管理。目前实践得比较成熟的是面向对象的思想,而对于前端开发,面向对象的诉求其实并不大。
有了面向对象这个前提后,对象的依赖、创建、生命周期管理等就成了一个问题,依赖注入 (DI) 正是提供了一种标准方式来解决此问题。它将依赖的创建和销毁交给“容器”去管理,使用者只管用,不操心具体细节。这也是控制反转 (IOC) 思想的一种实现。
上面这段话说得比较抽象,现实一点,个人觉得它比较方便的解决了两类问题:

上下文相关的依赖注入。就是需要根具不同的上下文注入不同的实例,共享上下文的状态。
异步依赖的注入。

下面通过两个例子来解释。例一,解释上下文相关依赖问题。先看代码:
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
) {}
}
剩下的就交给容器帮你创建和管理对象了。
结语
开篇写得比较简单,主要是关于为什么要依赖注入的思考。接下来可能会逐步分享实践方面的一些东西,比如项目结构,分层,基础设施等具体问题的解决方案,欢迎关注。

退出移动版