五分钟掌握 JavaScript 中的 IoC

IoC,控制反转(Inversion of Control)。它是依赖倒置原则(Dependence Inversion Principle)的一种实现方式,也就是面向接口编程。IoC的实现借助于第三方容器,能够解耦具备依赖关系的对象,下降开发维护成本。html

接下来咱们一块儿经过一个完整的示例来进一步了解这些概念。git

一个亟待扩展的业务模块

首先,咱们来看一个示例:github

class Order{
    constructor(){}
    getInfo(){
        console.log('这是订单信息')
    }
}

let order = new Order('新订单');
order.getInfo()

以上代码为某系统的订单管理模块,目前的功能是输出订单信息。编程

为订单模块添加评价功能

随着业务的发展,须要对订单添加评价功能:容许用户对订单进行评价以提升服务质量。微信

很是简单的需求对不对?对原有代码稍做修改,增长评价模块便可:框架

class Rate{
    star(stars){
        console.log('您对订单的评价为%s星',stars);
    }
}
class Order{
    constructor(){
        this.rate = new Rate();
    }
    // 省去模块其他部分 ...
}

let order = new Order('新订单');
order.getInfo();
order.rate.star(5);

一个小小的改动而已,很轻松就实现了:新增一个评价模块,将其做为依赖引入订单模块便可。很快 QA 测试也经过了,如今来杯咖啡庆祝一下吧 ☕️函数

为模块添加分享功能

刚刚端起杯子,发现 IM 上产品同窗的头像亮了起来:post

PM:若是订单以及评论可以分享至朋友圈等场景那么将会大幅提高 xxxxx

RD:好的 我调研一下测试

刚刚添加了评分模块,分享模块也没什么大不了的:优化

class Rate(){ /** 评价模块的实现 */}

class Share(){
    shareTo(platform){
        switch (platform) {
            case 'wxfriend':
                console.log('分享至微信好友');
                break;
            case 'wxposts':
                console.log('分享至微信朋友圈');
                break;
            case 'weibo':
                console.log('分享至微博');
                break;
            default:
                console.error('分享失败,请检查platform');
                break;
        }
    }
}

class Order{
    constructor(){
        this.rate = new Rate();
        this.share = new Share();
    }
    // 省去模块其他部分 ...
}

const order = new Order();
order.share.shareTo('wxposts');

此次一样新增一个分享模块,而后在订单模块中引入它。从新编写运行单测后,接下来QA须要对Share模块进行测试,而且对Order模块进行回归测试。

好像有点不对劲儿?能够预见的是,订单这个模块在咱们产品生命周期中还处于初期,之后对他的扩展/升级或者维护将是一件很频繁的事情。若是每次咱们都去修改主模块和依赖模块的话,虽然可以知足需求,可是对开发及测试不足够友好:须要双份的单测(若是你有的话),冒烟,回归...并且生产环境的业务逻辑和依赖关系远远要比示例中复杂,这种不彻底符合开闭原则的方式很容易产生额外的bug。

使用IoC的思想改造模块

顾名思义,IoC的主要行为是将模块的控制权倒置。上述示例中咱们将Order称为高层模块,将RateShare称为低层模块;高层模块中依赖低层模块。而IoC则将这种依赖关系倒置:高层模块定义接口,低层模块实现接口;这样当咱们修改或新增低层模块时就不会破坏开闭原则。其实现方式一般是依赖注入:也就是将所依赖的低层模块注入到高层模块中。

在高层模块中定义静态属性来维护依赖关系:

class Order {
    // 用于维护依赖关系的Map
    static modules = new Map();
    constructor(){
        for (let module of Order.modules.values()) {
            // 调用模块init方法
            module.init(this);
        }
    }
    // 向依赖关系Map中注入模块
    static inject(module) {
        Order.modules.set(module.constructor.name, module);
    }
    /** 其他部分略 */
}

class Rate{
    init(order) {
        order.rate = this;
    }
    star(stars){
        console.log('您对订单的评价为%s星',stars);
    }
}

const rate = new Rate();
// 注入依赖
Order.inject(rate);
const order = new Order();
order.rate.star(4);

以上示例中经过在Order类中维护本身的依赖模块,同时模块中实现init方法供Order在构造函数初始化时调用。此时Order便可称之为容器,他将依赖关系收于囊中。

再次理解IoC

完成了订单模块的改造,咱们回过头来再看看IoC:

依赖注入就是把高层模块的所依赖的低层次以参数的方式注入其中,这种方式能够修改低层次依赖而不影响高层次依赖。

可是注入的方式要注意一下,由于咱们不可能在高层次模块中预先知道全部被依赖的低层次模块,也不该该在高层次模块中依赖低层次模块的具体实现。

所以注入须要分红两部分:高层次模块中经过加载器机制解耦对低层次模块的依赖,转而依赖于低层次模块的抽象;低层次模块的实现依照约定的抽象实现,并经过注入器将依赖注入高层次模块。

这样高层次模块就脱离了业务逻辑转而成为了低层次模块的容器,而低层次模块则面向接口编程:知足对高层次模块初始化的接口的约定便可。这就是控制反转:经过注入依赖将控制权交给被依赖的低层级模块。

更简洁高效的IoC实现

上述示例中IoC的实现仍略显繁琐:模块须要显式的声明init方法,容器须要显示的注入依赖而且初始化。这些业务无关的内容咱们能够经过封装进入基类、子类进行继承的方式来优化,也能够经过修饰器方法来进行简化。

修饰器(Decorators)为咱们在类的声明及成员上经过元编程语法添加标注提供了一种方式。 Javascript里的修饰器目前处在 建议征集的第二阶段,但在TypeScript里已作为一项实验性特性予以支持。

接下来咱们就着重介绍一下经过修饰器如何实现IoC。

经过类修饰器注入

如下示例代码均为TypeScript

首先咱们实现低层模块,这些业务模块只处理本身的业务逻辑,无需关注其它:

class Aftermarket {
    repair() {
        console.log('已收到您的售后请求');
    }
}

class Rate {
    star(stars: string) {
        console.log(`评分为${stars}星`);
    }
}

class Share {
    shareTo(platform: string) {
        switch (platform) {
            case 'wxfriend':
                console.log('分享至微信好友');
                break;
            case 'wxposts':
                console.log('分享至微信朋友圈');
                break;
            case 'weibo':
                console.log('分享至微博');
                break;
            default:
                console.error('分享失败,请检查platform');
                break;
        }
    }
}

接下来咱们实现一个类修饰器,用于实例化所依赖的低层模块,并将其注入到容器内:

function Inject(modules: any) {
    return function(target: any) {
        modules.forEach((module:any) => {
            target.prototype[module.name] = new module();
        });
    };
}

最后在容器类上使用这个修饰器:

@Inject([Aftermarket,Share,Rate])
class Order {
    constructor() {}
    /** 其它实现略 */
}

const order:any = new Order();
order.Share.shareTo('facebook');

使用属性修饰器实现

Ursajs中使用属性修饰器来实现注入依赖。

Ursajs提供了@Resource修饰器和@Inject修饰器。

其中@Resource为类修饰器,它所修饰类的实例将注入到UrsajsIoC容器中:

@Resource()
class Share{}

@Inject为属性修饰器,在类中使用它能够将@Resource所修饰类的实例注入到指定变量中:

class Order{
    @Inject('share')
    share:Share;
    /** 其它实现略 */
}

在此以外,做为一个简洁优雅的框架,Ursajs还内置了寻址优化,能够更高效的获取资源。

没有银弹

虽然IoC很强大,但它仍然只是一种设计思想,是对某些场景下解决方案的提炼。它没法也不可能解决所有高耦合所带来的问题。而作为开发者,咱们有必要识别哪些场景适合什么方案。

小结

  • 复杂系统中高耦合度会致使开发维护成本变高
  • IoC借助容器实现解耦,下降系统复杂度
  • 装饰器实现IoC更加简洁高效
  • 没有银弹

参考

🕯️ R.I.P.

相关文章
相关标签/搜索