1、前言
如今的编程模型有很多种,常用的是面向过程编程(POP)、面向对象编程(OOP)。其实还有好几种编程模型:面向切面编程(AOP,也就是我们今天要讨论的主题)、响应式编程、函数式编程。每种编程模型都有其对应的应用场景,今天我们只讨论AOP(顺带捎上IoC),其他几种后面有时间可以拿出来学习。
2、AOP和IoC的介绍
2.1、AOP简述
AOP(Aspect Oriented Programming)主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。它是对传统OOP编程的一种补充。
OOP是关注将需求功能划分为不同的并且相对独立,封装良好的类,并让它们有着属于自己的行为,依靠继承和多态等来定义彼此的关系;AOP是希望能够将通用需求功能从不相关的类当中分离出来,能够使得很多类共享一个行为,一旦发生变化,不必修改很多类,而只需要修改这个行为即可。
2.2、IoC简述
IoC(Inversion of control)控制反转,它不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是 松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。
谈到IoC就不得不说DI(dependency injection)依赖注入,这个概念是大师级人物Martin Fowler在2004年提出的,只是为了更明确地描述IoC(IoC的另外一种实现方式叫做)。所以二者本质是一样的。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。
2.3、 AOP想要解决的痛点
假设有这么一个计算器类(Typescript):
interface Calculator {
add: (number num1, number num2) => number
sub: (number num1, number num2) => number
div: (number num1, number num2) => number
mul: (number num1, number num2) => number
}
class Cal implements Calculator {
constructor() {}
add(number num1, number num2) {
return num1 + num2
}
sub(number num1, number num2) {
return num1 - num2
}
div(number num1, number num2) {
return num1 / num2
}
mul(number num1, number num2) {
return num1 * num2
}
}
然后你想要在每一个计算方法中添加追踪日志,于是改造成:
interface Calculator {
add: (number num1, number num2) => number
sub: (number num1, number num2) => number
div: (number num1, number num2) => number
mul: (number num1, number num2) => number
}
class Cal implements Calculator {
constructor() {}
add(number num1, number num2) {
console.log(`add method calling, params: num1[${num1}],num2[${num2}]`)
const result = num1 + num2
console.log(`add method ending, result: [${result}]`)
return result
}
sub(number num1, number num2) {
console.log(`sub method calling, params: num1[${num1}],num2[${num2}]`)
const result = num1 - num2
console.log(`sub method ending, result: [${result}]`)
return result
}
div(number num1, number num2) {
console.log(`div method calling, params: num1[${num1}],num2[${num2}]`)
const result = num1 / num2
console.log(`div method ending, result: [${result}]`)
return result
}
mul(number num1, number num2) {
console.log(`mul method calling, params: num1[${num1}],num2[${num2}]`)
const result = num1 * num2
console.log(`mul method ending, result: [${result}]`)
return result
}
}
然后你可能还需要添加参数校验,于是又有各种校验逻辑加入,并且这种非业务的需求还会不断增加,于是就会不断添加重复代码,对于一个开发人员来说,新做一个需求就会不断地copy-paste。然后哪一天你需要改动日志显示方式,很可能就需要改所有的地方,一旦某处漏改,就造成显示不一致。由此可见这种代码是很难维护的。
那么针对这些痛点,AOP提出了横切关注点(crosscutting concern)的概念,这些与业务无关但是属于系统范围的需求并且会横跨多个模块的功能称为横切关注点。使用AOP编程模型,我们可以简化出这样的一张图:
看这个图是不是很像expressjs的中间件的角色?
2.4、IoC想要解决的痛点
在实际网关开发中,我们会创建很多service,诸如redis、disconf、logger、cache之类的,然后我们这样使用:
比如:
在controller1中用到redis: import redis from '...'
在controller2用到cache: import cache from '...'
大家都觉得这种写法没有什么不好,很符合我们的编程思维。但是这种写法有一个很大的问题,那就是耦合性太高。如果某一天你需要扩展redis service的实现方式,,比如需要增加一个参数,告知redis service去连接一个性能更好的redis数据库,这个时候所有引用到redis服务的都需要更改代码。
举个更具体的例子(typescript):
class Finder {
find: (...) => {...}
}
class Fridge {
finder: Finder
constructor() {
this.finder = new Finder()
}
getApple() {
return this.finder.find('apple')
}
}
在上面的例子中我们看到创建一个冰箱
类,提供一个查找苹果的方法,但具体怎么查找是使用另外一个实例Finder
的。这样看起来一切都是ok的。但是如我们刚才说的,如果现在我们想要增加一个参数,保证Finder
类查找的东西肯定位于冷藏室
呢?于是我们就需要改造类Finder
,改造完类Finder
还需要改造类Fridge
,如果在系统中我们有很多类同时用到了这个Finder
呢?那么就得改动很多地方,万一有漏改的呢?
结合上面的例子和实际应用,我们的IoC便是要处理这样的耦合。我们借助容器的概念,将类的创建和查找都放在容器中实现,Fridge
类不用关心Finder
类的创建,只需要向容器要求使用Finder
类,其他的一概不care,这样就将实体类(concrete class)与抽象类解耦掉,改造之前如图所示:
改造后:
3、 AOP在点我达网关的应用
在点我达网关项目开发中,一条request完整的链路大致如下:
相信这个处理过程,在别的公司也都是成立的。所有的controller代码除了业务逻辑不一样外,剩余的全都是一个套路,都是一套重复代码。起初网关的第一个框架版本就是这样设计的,而引入AOP之后,我们得到的效果图是这样的:
相对比于上面的那张图,这个改造可以看出我们将所有无关业务的需求作为切面抽出去,不再跟业务逻辑耦合,统一一个地方去维护代码,让整个网关的可维护性大大提高。代码量的大量减少也让开发人员可以更加专注于需求的开发。另外给整套网关带来了很大的灵活性和扩展性。
4、IoC在点我达网关的应用
在点我达网关项目开发,我们借助InversifyJS来实现IoC容器,实现的框架如下:
其他层次我们不必关注,有兴趣可以私聊。在容器层中我们会在服务器启动的时候主动创建多个实例,这些实例由容器维护并查找,当我们在中间件中或者业务逻辑层中需要用到这些实例的时候,只需要这么使用即可:
@lazyInject(TYPE.Logger) private logger: winston.LoggerInstance
我们得controller类不会再去关注依赖类的初始化创建,而只管使用,就好比是你找女朋友,如果去婚介所的话,你是不用关心你想找的女孩子在哪里,而只需要到婚介所拿到女孩子的信息即可,而这个容器就类似于婚介所。
在这里就不过多地展开,有兴趣的可以参考inversifyJs/Wiki
最后
点我达前端在Nodejs网关的实践中积累了不少的经验,包括微服务化和异地多活改造,当然也有今天说的这些概念。我们在整套前端工程化体系中同样沉淀了一些符合大家需求的经验,包括组件库、前端框架封装、Nodejs网关框架设计、脚手架、网关运维监控、Mock服务器等等。后面有时间可以先分享一下微服务在点我达网关的实践。同时也欢迎大家探讨交流。