2021-06-17:nestjs及相关的设计思想和名词
由于工作需要,需要选择一个NodeJS的框架来写后端业务,由于很久没有Coding了,所以起初的选型逻辑也非常简单,基本是以4四点:A、背后的团队 B、成熟度 C、社区用户量 4、问题响应速度。很快变从EggJS、NestJS、ThinkJS及Koa/express几个热门框架中选择了EggJS。
这主要是考虑到:
- 符合以上的4点要求,可靠性比较强
- 自己之前有过一些简单的了解和应用。
不过在开发和使用过程中,由于不断要去一些社区看一些问题,没多久便看到不少开发者频繁提到NestJS,而且从一些介绍中了解,似乎NestJS非常与众不同,未来前景很好,甚至可以说NestJS才是真正解决了架构问题的服务端框架。这很快引起了我的好奇和注意。
于是自己开始花一些时间去了解学习这个框架,初看文档,NestJS在我们脑海中立即打下了下面一些标签:
- NodeJS中的Spring框架
- 几个概念:IoC、DI、AOP
- SOLID设计原则
- 装饰器写法
- TypeScript
其实4和5对于一直关注在前端领域的同学们来说并不稀奇,TS补充了JS弱类型的一些缺陷,而且语法对JS完全兼容,非常适合前端使用。关于装饰器,本来就是TS以及ES7中的新特性。不了解这块的通过看一些文档也可以很快熟悉起来,这里并没有什么值得去说的。
真正让我开了眼睛的反而是前3点,这些概念对于一些资深的后端及软件开发的同事来说可能并不算陌生。但是对于众多一直使用JS的前端开发者而言,还是比较新鲜的,尤其是没有参与过服务端开发的同学而言。
我虽然早年做JAVA以及在使用YUI和Angular时对这些概念有过一些了解,但这么多年过去,也都忘得差不多了。这几天经过各种Google,重新温习了下这些对于软件设计来说非常重要的知识,我们一个个来说一下。
一、 Spring框架
Spring框架是Java中一个风行多年的框架。尽管很多人不喜欢JAVA框架,但也不得不说,这么多年的语言及框架发展下来,不少好的软件设计思想在JAVA的诸多框架中融入已经非常完善。Spring能够风行这么多年也是如此。
很重要的就是在Spring中融入了比如OOP、IoC、DI等优秀的软件设计思想,结合JAVA语言的特点,实现了很好的扩展性、灵活性、可维护性。对于大型的软件开发项目而言,这些点都非常重要。
二、IoC与DI
在我刚看到这两个概念时,查询了大量的文章和资料,但大部分的文章都讲的云里雾里,似乎都不得要点,对于没有任何相关语言和项目经验的人很难理解。有些说通过IOC降低对象的依赖,但是举的实例又都千篇一律,完全看不出来怎么就降低了依赖,看起来只是换了一个写法而已。
不过功夫不负有心人,再看了大量国内外文章后,自己终于有了一点理解。
先说概念本身:
IoC(Inversion of Control):中文翻译为控制反转。
DI(Dependency Injection):中文译为依赖注入。
那么什么叫控制反转?既然叫控制反转,必然应该是相对于控制正转的。一般我们在项目开发中,一定会遇到多个类(对象)之间的依赖关系。举个例子,一个汽车(Car类)是依赖一个发动机(Engine类)的,按照正常的思维习惯,我们的代码一般会这么写:
class Engine{
constructor(){
}
}
// Constructor injection
class Car{
constructor(){
this.engine = new Engine();
}
}
这种写法有几个问题:
- 对象之间形成强耦合。即Car强依赖于Engine,任何适合调用Car,必须同时引用到Engine。假如未来要替换或替换Car的Engine,只能修改Car这个类本身;或者如果Engine的参数结构做个改变,那么就需要修改所有依赖Engine的类。
- 对象复用性变得很低。比如Car现在强依赖于一个EngineA。假如我们需要一个使用EngineB的Car,就必须重写一个Car了。
- 测试复杂度变高。如果我们要测试Car的逻辑,这时候就必须执行Engine的完整逻辑,但Engine的逻辑可能很复杂,这对于我们的测试来讲毫无意义,只会造成损耗。在实际项目中,可能Engine的背后有各种三方调用,数据库操作等,但Car可能只是几个简单的Method。
以上的这种依赖关系,也叫做下层控制上层结构,因为一旦下层(Engine)改变,就需要去调整上层(Car)的代码,这种结构不符合软件设计中的依赖倒置原则(Dependency Inversion Principle)。比较好的设计应该是上层控制下层,即上层决定使用什么样的下层,然后由下层去具体实现。
IoC和DI便是为解决这个问题而产生。简单说,就是通过约定接口来定义不同对象之间的相互调用能力,但不在一个类的内部主动创建对象,而是通过特定方式注入到类中。
个人总结的一个原则:一个类对其它类的调用都通过注入的方式来使用,而不是通过诸如在内部初始化或者其它隐式传入(这在express/eggjs之类传统框架较为常见)。
最简单的依赖倒置代码结构:
class Engine{
constructor(){
}
}
// Constructor injection
class Car{
constructor(engine){
this.engine = engine;
}
}
const engine = new Engine();
const car = new Car(engine);
表面看起来,只是把engine实例由内部初始化改为外部初始化后传入。但这种结构从根本上解决了上面的几个问题。
- 只要约定engine的接口规范,未来可以有很多个engine类型和示例,而Car不需要关系到底是什么Engine,只要符合Engine的接口规范,都可以传入。
- Car有很强的复用性。不用关心具体的Engine,只要符合约定的接口。就可以使用这个Car类产生各类Engine不同的Car实例。
- 代码的测试性变强。我们在测试Car的时候,不需要传入真实的Engine,只要实现Engine接口即可,内部实现可以完全mock。
DI本质上和IoC讲的基本是一个意思,DI更像是IoC的一个具体实现方式。
DI(依赖注入)的方式除了通过构造器外(见上述的代码),还可以通过set方法注入。
// Setter injection
class Car{
constructor(){
}
setEngine(engine){
this.engine = engine;
}
}
不过我们无需关系具体的注入方式,更重要的是要知道这么一个小的变化背后的思想是什么,为什么要这么做,解决了什么问题。
除了前面的IoC和DI,还有一个概念叫AOP(Aspect Oriented Programming),中文译为:面向切片编程。单看这个单词或者翻译,很难直接理解其含义。
AOP可以这么理解,我们可以把一个业务看做很多个小业务逻辑组合而成。每个小业务逻辑可以看做是业务的一个切片,为了提高一些功能复用,我们可以通过AOP的方式,很方便在具体业务切片的前面或者后面添加特定的逻辑处理,从而改变原来的逻辑,从而实现特定的业务逻辑。
常见的应用场景一般为一些系统级的功能处理,如权限校验、缓存、错误处理等。系统设计上只要预留好钩子提供对于模块前后的拦截处理能力,即可视为支持了AOP。如koa的中间件模型,即可理解为是AOP的一种。
正因为有了AOP的模型,我们可以在控制器中只关心自身的业务逻辑即可,而不用每个业务逻辑中去重复处理类似缓存、授权等公共逻辑。这使得系统的结构变得更加灵活可扩展。
三、SOLID
无论说到的AOP,还是DI、IoC等,其实都源自于2004年Robert大叔提出的SOLID设计原则。SOLID是五大原则的首字母缩写,具体为:
- S: Single Responsibility Principle(单一职责原则)
- O: Open-Closed Principle(开放封闭原则)
- L: Liskov-Substitution Principle(Liskov替换原则)
- I: Interface Segregation Principle(接口聚合原则)
- D: Dependency Inversion Principle(依赖倒置原则)
由于解释比较长,而且网上有非常很多详细的解释,这里就不赘述了。可以点击上面的连接查看喜欢看中文的,也可以查看https://www.jianshu.com/p/1c6498da3862这篇文章做简单了解。
最后
了解上面这些关键的概念和思想,我们再来看restjs,就比较容易理解其代码结构及设计思想了,当然也能够更好去使用好它。
随着前端生态的不断拓展,尤其随着Nodejs在后端应用的深入,作为一个想做全栈开发的前端人员,千万不要仅仅把后端理解为对数据库的CRUD这么简单,而应该多请教后端的一些资深大牛们,深入去学习更多软件设计理念和思想。
唯有如此,才能成为一名真正的全栈工程师~
[参考资料]: