AOP&IOC&DI
参考文档
大笑文档 翻译文档
SOLID
S单一职责
O抽象类重写,对扩展开放对修改关闭(抽象类),接口不能保证overide
L子类必须能够替换成它们的基类,虚方法
I接口隔离,使用多个专门的接口比使用单一的总接口要更好(继承接口,子接口被实现)
D依赖注入
概念
IOC Inversion of Control
控制翻转,控制权从应用程序转移到框架架(如IOC容器),是一种设计模式。
const B = require('B.js');
const b = new B({});
如上是最基础的手动引入,还有很多设计模式可以实现依赖引入,大同小异,不反转时,只有A主动获取B之后,才能用到B,这是C也用到了B,C又引入了A,如果后面的需求B需要使用A的方法,这种重复引用互相关系比较乱。
反转就是A需要调用B,A不需要主动获取B,而是由其他人自动将B送上来,这个其他人就是容器,描述了依赖关系,在使用时容器来做管理
DI Dependency Injection
代码应当取决于抽象概念,而不是具体实现
高层模块不应该依赖于底层模块,二者应该依赖于抽象
抽象不应该依赖于细节,细节应该依赖于抽象
依赖注入,将相互依赖的对象分离,在配置中描述依赖关系,这些关系在应用时才被建立
如果IOC是一种设计模式,或者叫思想,那么DI是实现这种思想的一种方式,同时也可以通过观察者模式、模板方法模式等实现IOC
awilix是一个DI框架,用它在应用中实现IOC,创建容器对依赖关系做管理
非IOC架构中,应用程序来决定引用那些依赖,然后主动实例化;现在我们把这些工作交给IOC容器来完成,整个依赖对象的实例化、生命周期全部由容器接管,与业务代码高度解耦。
AOP:面向切面编程
AOP是一种编程范式,和面向过程、面向对象、函数式编程范式是同等概念。
是指在运行时,动态地将代码切入到类的指定方法,指定位置上的编程思想
举例:
日志埋点、权限校验等侵入性比较强,又比较完整的模块,可能需要在不少地方调用该模块的方法,按面向对象思想哪里用哪里引,方法遍地开花不好维护,此时面向切面编程就很好解决了该问题
function log(logData){
console.log(logData);
}
fuction Auth(userType){
if(userType > 0){
alert('无权限!');
}
}
function getRoute(key){
return this.$route[key];
}
const newGetRoute = function(key){
Auth(user.type),
getRoute(key),
log('some log')
}
上面的案例简单描述了log和Auth对getRoute方法的侵入,事实上,Auth和log此时变成了该方法两个pre和post的生命周期函数,在真正执行前后做了你想做的事情。redux里面到处都是。
总结
总而言之:抛开业务类型和项目规模谈架构思想都是耍流氓,用各种骚操作完成todoList,除了学习需要就是无病呻 吟。学习架构思想时又不能让业务喧宾夺主,只能用该思想完成较为简单的业务逻辑,咱们去体会,当你需要时你就 知道了。 引用张小龙说微信新功能的表述:
“你未看此花时,此花与汝同归于寂;你来看此花时,则此花颜色一时明白起来”;--王阳明
微信有很多强大的功能不去大势运营,不让他们打扰用户的的使用,当你需要用到时,发现他在 学架构、学思想也是这样,拥有这个能力后,不一定处处使用,当需要时,自然使出来,他就在那里。
前端解读控制反转(IOC)
随着前端的承担的职责越来越重,前端应用向着复杂化、规划化的方向发展。大型项目模块化是一种趋势,不可避免模块之间要相互依赖,此外还有很多第三方包要互相依赖。这样的话如何去管理这些繁杂的文件,是一个不可避免的话题。此时作为一种已经被实践证明过的思想模式一直得到大家的青睐,这就是控制反转(IOC)。
IOC定义
控制反转是面向对象编程中的一种设计模式,可以降低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(dependency Injection简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转对象在被创建的时候由一个调控系统内所有对象的外接实体,将其所依赖对象的引用传递给他,也可以说以来被注入到对象中
原则
- 高层模块不应该依赖低层模块。两个都应该依赖抽象
- 抽象不应该依赖具体实现
- 面向接口编程,而非面向实现编程
目的
降低耦合,提高扩展性
实例
class RTeam{
constructor(){
this.name = "火箭";
}
}
class Player(){
constructor(){
this.team = new Team();
}
info(){
console.log(this.team.name);
}
}
let ym = new Player();
ym.info();//火箭
球员player依赖某个球队RTeam当调用的时候主动去加载球队即可。此时的控制权在player这里。
假如这时候球员发生交易了,球队信息更换了,转换到team2了。这时候我们就需要去修改player里的代码了,因为球员哪里直接写死了对RTeam的依赖,这种可扩展性是很差的。这不是我们所想要的,需要重新思考下依赖关系处理了。球员和球队之间是不是必须要直接关联,一个球员对应个球队的话,未来会发生变化的可能性太大了,毕竟不止一个球队,如果两者不直接发生联系就需要一个中间模块来负责两者关系的处理,球员不关注球队从哪里来,主要给他就行,这样控制权就不直接落在player上,这就是IOC的设计思路。
依据IOC改进
参照如下原则
- 高层模块不应该依赖低层模块,两个都应该依赖抽象 这里的player就是高层模块,直接依赖了球队这个低级模块,所以我们将两者解耦,player不在直接依赖于Team这个class
- 抽象不应该依赖具体实现,具体实现应该依赖抽象,具体到这里看player模块不应该直接依赖具体team,而是通过构造函数将抽象的TeamInfo实例传递进去,这样就是解耦具体实现
//球队信息不依赖具体实现
//面向接口即面向抽象编程
class TemoInfo{
constructor(name){
this.name = name;
}
}
class Player{
//此处的参数是teamInfo的一个实例,不直接依赖具体的实例
//面向抽象
constructor(team){
this.team = team;
}
info(){
console.log(this.team.name);
}
}
//将依赖关系放到此处来管理,控制权也放到了此处
//Player和TeamInfo之间不再有直接依赖
//原本直接掌握teamInfo控制权的player不再直接依赖
//将依赖控制,落在此处(第三方模块专门管理)即控制反转
var ym = new Player(new TeamInfo('⽕箭'))
ym.info()
var kobe = new Player(new TeamInfo('湖⼈'))
kobe.info()
这⾥发现, TeamInfo和Player之间已经没有直接关联了,依赖关系统⼀放到getTeamInfo中。 所谓控制 反转就如何上⾯⼀样,将依赖的控制权由player转移到其他地⽅即我们专⻔的依赖管理来做了。 这样再 增加⼀个team3,改动也不⼤,复⽤就⾏了
实现
上⾯其实就是最简单的IOC实现了,基于IOC的编程思想,主要有两种实现⽅式:依赖注⼊和依赖查找。 依赖查不太常⽤,常⻅的是依赖注⼊。
依赖注入
组件之间的依赖关系由容器在运行期决定,形象的来说即由容器动态的将某种依赖关系注入到组件之中。
在Require.js/AMD的模块加载器的实现就是基于依赖注入来的,还有angularjs其实也是使用了大量的依赖注入
总结
控制反转这里控制权从使用这本身转移到第三方容器上,而非是转移到被调用者上,控制反转是一种思想,依赖注入是一种设计模式。
javascript中的依赖注入
我喜欢引⽤这样⼀句话‘编程是对复杂性的管理’。可能你也听过计算机世界是⼀个巨⼤的抽象结构。我们 简单的包装东⻄并重复的⽣产新的⼯具。思考那么⼀下下,我们使⽤的编程语⾔都包括内置的功能,这 些功能可能是基于其他低级操作的抽象⽅法,包括我们是⽤的javascript。 迟早,我们都会需要使⽤别 的开发者开发的抽象功能,也就是我们要依赖其他⼈的代码。 我希望使⽤没有依赖的模块,显然这是很 难实现的。即使你创建了很好的像⿊盒⼀样的组件,但总有个将所有部分合并起来的地⽅。 这就是依赖 注⼊起作⽤的地⽅,当前来看,⾼效管理依赖的能⼒是迫切需要的,本⽂总结了原作者对这个问题的看 法。
目标
假设我们有两个模块,一个是发出ajax请求的服务,一个是路由。
const service = function(){
return {name:'Service'}
}
const route = function(){
return {name: 'Router'}
}
//下面是另一个依赖了上述模块的函数
const doSomething = function(other){
var s = service();
var r = route();
}
该函数接受一个参数。当然可以使用上面的代码,但是这不太灵活,如果我们想要使用ServiceXML、ServiceJSON,后者我们想要mock一些测试测模块,这样我们不能每次都是编辑函数体,为了解决这个现状,首先提出将依赖当做参数传递给函数
const doSomething = function(service, route, other){
const s = service();
const r = route();
}
这样我们就把需要的模块的实例传递了过来,然而这样有个新的问题,想一下如果dosomthing函数在很多地方被调用,如果有第三个依赖条件,我们不能改变所有的调用doSomething的地方,如果我们有很多地方用到了doSomthing
//a.js
var a = doSomething(service, route, 1);
//b.js
var a = doSomething(service, route, 2);
//假如依赖条件改了,即doSomeThing需要第三个依赖才能正常工作
//这时候就需要上面不同文件中修改了,如果文件够多,就不合适了
const doSomething = function(service,router,third,thother){
const s = service();
const r = router();
}
所以需要一个帮助我们来管理依赖的工具,这就是依赖注入想要解决的问题,如下是想要达到的目标:
- 可以注册依赖
- 注入器应该接受一个函数并且返回一个已经获得需要资源的函数
- 我们不应该写复杂的代码,需要简洁优雅的语法
- 注入器应该保持传入函数中作用域
- 被传入的函数应该可以接收自定义的参数,不仅仅是被描述的依赖
requirejs/AMD的方式
define(['service', 'router'], function(service,router){
//...
})
这种思路是首先声明需要的依赖,然后开始编写函数,这里参数的顺序是很重要的,如下编写一个injector的模块。
cosnt doSomething = injector.resolve(['service', 'router'], function(service, router,other){
expect(service().name).to.be('service');
expect(router().name).to.be('router');
expect(other).to.be('other');
});
doSomething('other');
doSomething的函数体,使⽤expect.js来作为断⾔库来确保我的代码能像期望那样正常⼯作。体现了⼀点点TDD(测试驱动开发) 的开发模式。
如下是一个injector模块的开始,一个单例模式是很好的选择,因此可以在我们应用的不同部分运行
//IOC容器
const injecter = {
//保存所有的依赖模块
dependencies: {},
//寄存器,增加模块依赖
register:function(key,value){
this.dependencied[key] = value;
}
//返回函数,将需要的依赖注入到函数中
resolve:fcuntion(deps,func,scope){
}
}
从代码来看,确实是一个简单的对象,有两个函数和一个作为存储队列的变量,我们需要做的是检查deps依赖数组,并且从dependencies队列中查找答案。剩下的就是调用.apply方法来拼接被传递过来函数的参数。
resolve:fcuntion(deps,func,scope){
const args = [];
//处理依赖,如果依赖队列中不存在对应的依赖模块,显然该依赖不能被调用需要抛出错误
for(let i = 0, length = deps.length,d=deps[i],i<length;i++){
if(this.dependencies[d]){
args.push(this.dependencies[d]);
}else{
throw new Error(`can/'t resolve ${d}`);
}
}
return function(){
//将类数组对象转换为数组和查找到的依赖数组进行合并
func.apply(scope||{},args.concat(Array.prototype.slice.call(arguments, 0)));
}
}
如果scope存在,是可以被有效传递的。 Array.prototype.slice.call(arguments,0)将arguments(类数组)转换成真正的数组。 ⽬前来看很不错的,可以通过测试。当前的问题时,我们必须写两次需要的依赖,并且顺序不可变动,额外的参数只能在最后⾯。
反射
反射是程序在运行时可以检查和修改对象结构和行为的一种能力,简而言之在js上下文中,是指读取并且分析对象或者函数的源码。doSomething.toString();
"function(service,router,other){
const s = service();
const r = router();
}"
这种将函数转换成字符串的方式赋予我们获取预期参数的能力,并且最重要的是,他们的name,下面是angular依赖注入实现方式,如下是Angular获取arguments的正则表达式:
/^function\s*[^\(]*\(\s*([^\)\)/m
修改resolve方法
const injector = {
dependencies: {},
regiester(key, value) {
return this.dependencies[key] = value
},
//func,scope
resolve() {
//args传给func的参数数组,包括依赖模块和自定义参数
let func, deps, scope, args = [], self = this;
//获取传入的func,为了接下来的拆分字符串
func = arguments[0];
//正则拆分,获取依赖模块的数组
deps = func.toString().match(/^function\s*[^\(]*\s*\((\s*([^\(]*)\s*)\)/m)[2].replace(/ /g, '').split(',');
//待绑定作用域,不存在则不指定
scope = arguments[1] || {};
return function () {
//将arguments转为数组
//即后面再次调用的时候,doSomething('other')
//这里的other就是a,用来补充缺失的模块
const a = Array.prototype.slice.call(arguments, 0);
//循环依赖模块数组
for (let i = 0, length = deps.length; i < length; i++) {
const d = deps[i];
//依赖队列中模块存在且不为空的话,push进参数数组中
//依赖队列中不存在对应模块的话从a中取第一个元素push进去(shift之后数组在变化);
args.push(self.dependencies[d] && d !== '' ? self.dependencies[d] : a.shift());
}
console.log(args);
func.apply(scope, args);
}
}
}
function service() {
console.log('Service');
}
function route() {
console.log('Service');
}
//加入依赖库
injector.regiester('service', service);
injector.regiester('route', route);
const doSomething = injector.resolve(function( service, name, age, route ) {
console.log(service);
console.log(name);
console.log(age);
console.log(route);
}, { type: '类型' });
doSomething('石晓波', 26);
循环变异数组依赖项,如果缺少某些东西,可以尝试在arguments对象中获取,当数组为空是shift方法会返回undefined而不是报错
如上的代码不用重复声明,参数顺序也可变,实现上复制了angular的魔力,然而这并不完美,压缩会破坏我们的逻辑,这是反射注入的一大问题,因为压缩改变了参数的名称,导致依赖关系查找遭到破坏
//根据key来匹配就是有问题的
const doSomething = function(e,t,n){const r = e();const i = t()};
angular团队的解决方案如下
const doSomething = injector.resolve(['service', 'router', function(service,router){
}]);
看起来和开始的requirejs的方式一样了,没有更好的解决方案,为了适应这两种情况,最终解决方案如下。
function service() {
console.log('Service');
}
function router() {
console.log('Router');
}
const injector = {
dependencies: {},
register(key, value) {
this.dependencies[key] = value;
},
resolve() {
let func, deps, scope, args = [], self = this;
if (typeof arguments[0] === 'string') {
func = arguments[1];
deps = arguments[0].replace(/ /g, '').split(',');
scope = arguments[2];
} else {
//根据规定,如果第一个参数不是字符串那么走反射的第一种方式
func = arguments[0];
deps = func.toString().match(/^function\s*[^\(]*\((\s*([^\)]*))\)/m)[1].replace(/ /g, '').split(',');
scope = arguments[1];
}
return function () {
const a = Array.prototype.slice.call(arguments, 0);
for (let i = 0, length = deps.length; i < length; i++) {
const d = deps[i];
args.push(self.dependencies[d] && d !== '' ? self.dependencies[d] : a.shift());
}
func.apply(scope || {}, args);
}
}
}
//注册到依赖库
injector.register('service', service);
injector.register('router', router)
// const doSomething = injector.resolve('service,,router', function (a, b, c) {
// console.log(a);
// console.log(b);
// console.log(c);
// });
// doSomething('otner');
const doSomething = injector.resolve(function ( service, name, age, router ) {
console.log(service);
console.log(name);
console.log(age);
console.log(router);
}, { type: '类型' });
doSomething('石晓波', 26);
直接注入作用域
有时候我们使用第三种方式,它涉及到函数作用域的操作
function service() {
console.log('Service');
}
function router() {
console.log('Router');
}
const injector = {
dependencies: {},
register(key, value) {
return this.dependencies[key] = value;
},
resolve(deps, func, scope = {}) {
let self = this;
for (let i = 0, length = deps.length; i < length; i++) {
let d = deps[i];
if (self.dependencies[d]) {
scope[d] = self.dependencies[d];
} else {
debugger;
throw new Error(`con\'t resolve ${d}`);
}
}
return function () {
let a = Array.prototype.slice.call(arguments, 0);
func.apply(scope || {}, a);
}
}
}
//注册到依赖库
injector.register('service', service);
injector.register('router', router)
const doSomething = injector.resolve(['service','router'], function (a, b, c) {
console.log(a);
console.log(b);
console.log(c);
console.log(this);
});
doSomething('hello', 'how', 'are');
面向切面编程思想
编程思想的逐层提升
- 面向对象:传统方式动态引入需要的类
- 工厂模式:业务层不需要关注实例是怎么生成的
- 面向切面:不用再写工厂类,直接从IOC容器中创建好的实例取用
- oop是静态的抽象,aop是动态的抽象
SOLID
面向对象设计原则之一
- 单一功能 Single Responsibility(每个类功能单一,高内聚低耦合)
- 开闭原则 Open Close Principle(类实现接口,新功能实现新的类)
- 里氏替换 Liskov Substitution Principle(子类可以替换任何基类出现的地方)
- 接口隔离 Interface Segregation Principle(使用多个专门的接口比使用单一接口好,类多实现几个接口)
- 依赖反转 Dependency Inversion Principle(解除对象之间互相的依赖关系,通过IOC容器控制类之间的依赖关系,通过构造注入的方式)
面向切面编程
面向切面编程给我们提供了一个方法,让我们可以在不修改目标逻辑的情况下将代码注入到现有的函数或对象中
虽然不是必须的但是注入的到吗意味着具有横切关注点,比如添加日志功能,调试元数据或其他不太通用的但是可以注入额外的行为,尾部影响原始代码内容
距离:假如已经写好业务逻辑,但是现在要添加日志代码,通常方法是将日志逻辑几种到一个新的模块中,然后逐个函数添加日志信息
最好的实现方式就是:通过获取同一个日志程序,在想要记录的每个方法执行过程中特定节点。只需要一行代码就可以将程序注入,会带来很多便利
切面、通知和切点(是什么、在何时、在何地)
- 切面(是什么):这是你想注入到你的目标代码的切面或者行为,在我们的上下文环境(Javascript)中,这指的是封装了你想要添加的行为代码
- 通知(在何时):希望这个切面什么时候执行?“通知”制定了你想要执行切面代码的一些常见时刻比如“before”、after、around、whenThrowing等等。返过来它指的是与代码执行相关的时间点,对于在代码执行后引用的部分,这个切面将拦截返回值,并可能在需要时覆盖它
- 切点(在何地)他们引用了你想要注入的切面在你目标代码中的位置。理论上你可以明确指定目标代码中的任何位置去执行切面代码。实际上这并不现实,到那时可以潜在的指定,比如对象中的所有方法或者仅仅是一个特定指定,甚至可以使用所有以get_开头的方法之类的内容
创建一个AOP的库来向现有的基于OO的业务逻辑添加日志逻辑,需要做的就是用一个自定义的函数替换目标对象现有的匹配方法,该自定义函数会在适当的时间点添加切面逻辑然后调用原有方法
//用于获取一个对象中所有方法的帮助函数
//获取原型上的方法是因为class语法糖会将函数默认绑定到prototype
const getMethods = (obj) => Object.getOwnPropertyNames(Object.getPrototypeOf(obj)).filter((item) => typeof obj[item] === 'function');
//将原始方法替换为自定义函数,该函数在通知指示时调用切面
//参数:目标对象、目标函数名称、切面、通知
function replaceMethod(target, methodName, aspect, advice) {
//保存目标函数代码
const originalCode = target[methodName];
//重写目标函数
target[methodName] = (...args) => {
//根据通知判断切入点的位置,也就是切面执行的地点(当前是主函数执行前)
if (["before", "around"].includes(advice)) {
aspect.apply(target, args);
}
const returnValue = originalCode.apply(target, args);
//根据通知判断切入点的位置,也就是切面执行的地点(当前是主函数执行后)
if (['after', 'around'].includes(advice)) {
aspect.apply(target, args);
}
//将主函数运行后的返回值传递给切面函数
if ('afterReturning' == advice) {
return aspect.apply(target, [returnValue])
} else {
return returnValue;
}
}
}
module.exports = {
//导出的主要方法:在需要的时间和位置将切面注入目标
//参数:目标对象、切面、通知、切入点、方法
inject: function (target, aspect, advice, pointcut, method = null) {
//切入点
if (pointcut == 'method') {
replaceMethod(target, method, aspect, advice);
}
if (pointcut == 'methods') {
const methods = getMethods(target);
methods.forEach(m => {
replaceMethod(target, m, aspect, advice);
})
}
}
}
const AOP = require('./aop');
class MyBussinessLogic {
add(a, b) {
console.log('Calling add');
return a + b;
}
concat(a, b) {
console.log('Calling concat');
return a + b;
}
power(a, b) {
console.log('Calling power');
return a ** b;
}
reduce(a) {
console.log('Calling reduce');
return a--;
}
}
const o = new MyBussinessLogic();
function loggingAspect(...args) {
console.log("== Calling the logger function ==");
console.log("Arguments received: " + args);
}
function printTypeOfReturnedValueAspect(value) {
console.log("Returned type: " + typeof value)
}
function reduceAspect(...args) {
console.log('reduce aspect' + args);
}
AOP.inject(o, loggingAspect, 'before', 'mehods');
AOP.inject(o, printTypeOfReturnedValueAspect, 'afterReturning', 'methods');
AOP.inject(o, reduceAspect, 'before', 'method', 'reduce');
o.add(2, 2);
o.concat('hello', 'AOP');
o.power(2, 3);
o.reduce(5);
AOP的优点汇总
- 封装横切关注点的好方法,更容易阅读和维护可以在整个项目中重复使用的代码
- 灵活的逻辑,在注入切面时,围绕通知和切入点实现的逻辑可以提供很大的灵活性,反而有助于动态地打开和关闭代码逻辑的不同切面
- 跨项目重复使用切面,你可以将切面视为组件,即可以在任何地方运行的小的,解耦的代码片段,如果正确的编写切面代码,可以轻松地在不同的项目中共享代码
AOP的问题
AOp的主要优势是隐藏了代码逻辑和复杂性,如果不清楚AOP里面的逻辑会产生副作用,面向切面编程是OOP的补充,特别得益于javascript的动态特性,我们可以非常容易的实现它,它提供了强大的功能,能够对大量的逻辑进行模块化和解耦,以后可以与其他项目共享逻辑如果不能正确的使用,会吧代码搞的一团糟
AOP(Aspect Oriented Programming),通过预编译和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的的一个热点,是函数式编程中的一中衍生泛型,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
基础概念:aop完善spring的依赖注入(DI)面向对象编程将程序分解成各个层次的对象,面向切面编程将程序运行过程分解成各个切面。
Filter(过滤器):也是一种AOP的A,它利用了一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到可重用的模块,并将其命名为Aspect,即切面
优点AOP的好处就是你只需要干你自己的正事,其他事情别人帮你干,在你访问数据之前,自动帮你开启事务,当你访问数据库结束之后,自动帮你提交/回滚事务