在 NestJS 中面向接口开发
「当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。」
相信很多人都知道「鸭子类型(Duck typing)」这个东西,它可以说是对面向接口开发最形象的一个例子了。在 NestJS 中,可以通过模块轻松实现「面向接口」编程。
本文中,假定了一个「发送通知」的业务场景,并在此实现相应的功能。
接口定义与实现
既然面向接口,那么首先便需要「接口」。它定义了一个库应该实现的接口,并让调用该库(程序或开发人员)可以按照接口的定义直接使用该库。
从发送通知的功能考虑,一个能够发送通知的库应该是什么样子呢?首先需要一个publish()
方法,同时我们还需要一个方法 history()
能够获取历史发送的消息,所以接口的定义是这样的:
1 | // src/app/message/message.interface.ts |
这样,任何实现了这个接口的类我们都可以发送消息和查看历史记录了,前提是库正确地实现了这个接口。例如:
1 | // src/app/message/dingtalk.message.ts |
如果再需要实现一个 Slack 的消息推送,那么再加一个类即可:
1 | // src/app/message/slack.message.ts |
同理,还可以实现邮件推送、微信推送等等。
使用实现了接口的类
很自然地,我们在 NestJS 中定义了服务类的类,那么便交给 NestJS,让它实例化后丢进容器池里:
1 | // src/app/message/message.module.ts |
上述代码中的 MessageService
便是调用我们实现的类的地方了:
1 | // src/app/message/message.service.ts |
现在看起来这么调用有点蠢,我们不可能在代码中硬编码这种东西,不然这样子「面向接口」也没意义了。接下来对它进行简单的改造,要知道第一个参数 config
一直没有提到过,这里它开始派上用场了:
1 | () |
这样子便可以通过调用时传入的参数切换不同的发布方式了,至于参数是从哪来的呢?可以是用户通过 HTTP 请求传递上来的,也可以是数据库中的一条记录。这个 config
中还可以包含不同发布方式所需要的不同参数,如密钥、token 之类。
讲到这里似乎跟 NestJS 还扯不上太多不一般的关系,不过我们试想,这种写法在每增加一种发布类型的时候,除了实现对应的类,在 MessageModule
中交给 NestJS 管理,还需要对 MessageService
的代码进行改动。
了解过 NestJS 如何处理循环依赖的同学应该对 ModuleRef
不陌生,这里我们便通过它来进一步简化这部分功能代码:
1 | () |
使用 ModuleRef
从 NestJS 容器池内根据 Provider Name
找到对应的实例,而这个 Provider Name
是通过计算得到的:将 config.type
的首字母大写,然后与 Message
拼接起来得到类名,当然这要求我们按照这一规则对类进行命名。不过这不是什么大问题。
最后提一下,上述功能是在一个「模块」中实现的,实际上它们并不需要一定在一个模块中,在外部引入的模块中只要可以正常注入的,都可以通过 ModuleRef
获取:
1 | // src/app/message/dingtalk/dingtalk.message.ts |
这种形式我通常用于后续需要添加平行的功能(适配器)来进行外部服务与内部核心服务对接的的时候,方便日后扩展。