BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

当IoC遇见了Node.js

| 作者 倪震洋 关注 0 他的粉丝 发布于 2014年6月6日. 估计阅读时间: 25 分钟 | GMTC大前端的下一站,PWA、Web框架、Node等最新最热的大前端话题邀你一起共同探讨。

没有IoC的年代

一个简单的例子:

var Engine = require('./engine');
var Wheel = require('./wheel');

var Car = function() {
  this.engine = new Engine();
  this.wheel = new Wheel();
}

Car.prototype.run = function() {
  this.engine.run();
  this.wheel.run();
  console.log('run car...');
}

module.exports = Car;

在例子中,汽车(car)需要依赖轮子(wheel)和发动机(engine)才能跑起来。为了处理好这一关系,必须首先人为的通过require把engine和wheel引入进来,然后通过new操作实例化,这样car才能真正的run起来。

这个例子非常简单,但是存在着一些问题:

  1. require的时候,需要直接知道engine和wheel的文件位置、文件名、以及所exports的是什么。一旦engine或者wheel的文件名、所在位置、exports方式发生了变化,require操作必须做出相应的改变
  2. car直接依赖于engine和wheel,因此如果我们试图做单元测试,则会发现mock engine或者wheel非常困难,要么就是修改engine、wheel的代码,要么就是修改car的代码

这个例子只有3个对象,读者可能觉得也没啥要紧的,这样直接做也没多大问题。但是一旦系统里面的对象数量变大了呢?复杂的依赖关系可能就是这样的:

这样的系统紧密耦合,往往会造成难以维护、难以重构、难以做单元测试,尤其是当一个新人加入团队的时候,也会因为这份复杂性变得举步维艰,看不明白也改不动。

步入IoC

使用IoC之后,car的代码就会变成如下所示:

var Car = function(engine) {
  this.engine = engine;
  this.wheel = null;
}

Car.prototype.run = function() {
  this.engine.run();
  this.wheel.run();
  console.log('run car...');
}

module.exports = Car;

car无需知道engine、wheel的具体所在以require进来,也无需知道engine和wheel什么时候实例化以调用run方法跑起来,一切都变得如此简单与美好!

  1. 去除了engine和wheel的直接依赖,随便engine和wheel叫什么名字,写在哪里(甚至可以是一个remote对象),重构变得轻而易举
  2. 想对car进行单元测试,只需要依赖注入一个mock的engine和wheel对象即可完成,再也不需要直接修改car或者engine、wheel的代码了

让IoC发挥作用

本文通过Bearcat所提供的IoC容器来让IoC在Node.js中发挥作用。

Bearcat IoC 使用非常简单,只需要提供一个简单的配置文件即可让IoC容器管理下的系统运转起来:

{
  "name": "simple_inject",
  "beans": [{
    "id": "car",
    "func": "car",
    "props": [{
      "name": "wheel",
      "ref": "wheel"
    }],
    "args": [{
      "name": "engine",
      "ref": "engine"
    }]
  }, {
    "id": "wheel",
    "func": "wheel"
  }, {
    "id": "engine",
    "func": "engine"
  }]
}

这里就通过一个简单的context.json配置文件来对IoC进行了描述:告知容器中有一个car,依赖于wheel和engine,wheel通过对象属性的方式注入,engine通过构造函数参数的方式注入,容器中还有一个wheel和一个engine。

启动容器跑起来,只需要把context.json的路径传递给bearcat即可:

var Bearcat = require('bearcat');
var contextPath = require.resolve('./context.json');

var bearcat = Bearcat.createApp([contextPath]);
bearcat.start(function(){
   var car = bearcat.getBean('car'); // get bean
   car.run(); // call the method
});

运行结果

[2014-05-05 18:50:41.996] [INFO] bearcat - [app] Bearcat startup in 6 ms
run engine...
run wheel...
run car...

更多IoC的功能

scope 定义

IoC中可以定义scope,可支持singleton和prototype两种scope,默认情况下scope是singleton的。scope其实对应着常见的两种设计模式,即单例模式(singleton)和多例模式(prototype)。

singleton:

{
    "name": "simple",
    "beans": [{
        "id": "car",
        "func": "car",
        "scope": "singleton"
    }]
}
var car1 = bearcat.getBean('car');
var car2 = bearcat.getBean('car');
// car1 与 car2 是同一个实例对象

prototype:

{
    "name": "simple",
    "beans": [{
        "id": "car",
        "func": "car",
        "scope": "prototype"
    }]
}
var car1 = bearcat.getBean('car');
var car2 = bearcat.getBean('car');
// car1 与 car2 不是同一个实例对象 

生命周期回调

初始化与销毁操作在Node.js开发中是非常常见的。

初始化方法

var Car = function() {
    this.num = 0;
}

Car.prototype.init = function() {
    console.log('init car...');
    this.num = 1;
    return 'init car';
}

Car.prototype.run = function() {
    console.log('run car...');
    return 'car ' + this.num;
}

module.exports = Car;

car现在需要在实例化之后执行一个init方法来做些初始化的工作,那么在IoC中可以如下定义:

{
    "name": "simple_init_method",
    "beans": [{
        "id": "car",
        "func": "car",
        "scope": "prototype",
        "init": "init"
    }]
}

销毁方法

销毁方法在处理数据库链接等场景时非常有用。一个系统在shutdown的时候,平滑优雅的关闭就需要处理一些资源释放、完成未完成的任务等工作:

var Car = function() {

};

Car.prototype.destroy = function() {
    console.log('destroy car...');
    return 'destroy car';
};

Car.prototype.run = function() {
    console.log('run car...');
    return 'car';
};

module.exports = Car;

当car结束生命的时候,需要执行一个destroy方法来释放资源,那么在IoC中可以如下定义:

{
    "name": "simple_destroy_method",
    "beans": [{
        "id": "car",
        "func": "car",
        "destroy": "destroy"
    }]
}

异步初始化方法

众所周知,Node.js中异步调用是非常平常的,比如初始化一个MySQL或者Redis的连接都是异步的,那么异步的初始化方法也就不可避免。而且在某些场景下,必须要求异步操作完成后,才能继续另外一个操作,这就要求保证两者之间的顺序性。在 Bearcat IoC中,你可以配置orderasync来完成这样的初始化需求:

var Car = function() {
    this.num = 0;
}

Car.prototype.init = function() {
    console.log('init car...');
    this.num = 1;
}

Car.prototype.run = function() {
    console.log('run car...');
    return 'car ' + this.num;
}

module.exports = Car;
var Wheel = function() {}

Wheel.prototype.init = function(cb) {
    console.log('init wheel...');
    setTimeout(function() {
        console.log('asyncInit setTimeout');
        cb();
    }, 1000);
}

Wheel.prototype.run = function() {
    console.log('run wheel...');
    return 'wheel';
}

module.exports = Wheel;

在这个简单的例子中,wheel有一个异步的初始化方法,它必须在car初始化之前调用,那么你就可以在context.json配置中配置wheel为async的,且order的值比car的要小,以表明wheel要在car之前初始化:

{
    "name": "simple_async_init",
    "beans": [{
        "id": "car",
        "func": "car",
        "init": "init",
        "order": 2
    }, {
        "id": "wheel",
        "func": "wheel",
        "async": true,
        "init": "init",
        "order": 1
    }]
}

IoC 实战

随心所欲的单元测试

在单元测试中,很多情况下需要构造一个对象的 mock 对象出来,然后被测试的对象调用这个mock对象来进行单元测试。但是在没有IoC之前,由于测试对象和mock原对象之间往往是紧密耦合的,那么要完成这样的操作,要么就是修改mock原对象的代码,要么就是修改测试对象的代码,但这样都不是最佳的实践。比如说一个基于express的web应用里面有一个userController,它依赖于userService:

var userService = require('../service/user-service');

exports.allUsers = function (req, res, next) {
  userService.getUsers(function (err, users) {
    if (err) {
      return next(err);
    }
    res.json(users);
  });
};

这时,如果想构造一个userService的mock对象mockUserService,并且在userController里面require进来进行测试的话,就需要修改userController里面的代码,比如这样子:

//var userService = require('../service/user-service');
var userService = require('../service/mock-user-service');

exports.allUsers = function (req, res, next) {
  userService.getUsers(function (err, users) {
    if (err) {
      return next(err);
    }
    res.json(users);
  });
};

而通过IoC,这一切就变得非常的简单,无需修改代码即可完成。

在IoC容器管理下,依赖关系是通过对象给IoC容器的描述来完成的,因此,只需要修改context.json元数据配置,即可完成原始对象和mock对象之间的无缝切换:

{
    "name": "simple_unit_test",
    "beans": [{
        "id": "userController",
        "func": "userController",
        "props": [{
          "name": "userService",
          "ref": "userService"
        }]
    }]
}

改成如下所示的test-context.json即可。单元测试的时候,使用test-context.json来作为IoC容器的配置,既不影响开发,也可以完成测试工作,相当的便捷:

{
    "name": "simple_unit_test",
    "beans": [{
        "id": "userController",
        "func": "userController",
        "props": [{
          "name": "userService",
          "ref": "mockUserService"
        }]
    }]
}

一致性配置

在Node.js开发中,系统需要配置的参数本质上其实就是设置函数的参数或者对象的属性。

比如要创建一个Redis连接,就是传入一个Redis的host,port参数:

var serverConfig = require('../../config/server');
var redis = require("redis");
var client = redis.createClient(serverConfig['redisPort'], serverConfig['redisHost']);

client.on("error", function(err) {
  console.error("redis error " + err);
});

client.on("ready", function() {
  console.log("redis is ready");
});

module.exports = client;

上面的做法简单粗暴,但是配置往往是与环境相关的。开发环境、测试环境、线上环境的redis port、host都不一样,因此这样的做法就无法解决环境切换的问题,要么就只能根据不同环境来对config/server文件进行替换,做法相当的粗暴,很容易出现问题。

通过IoC,配置问题就将的变得非常简单,环境切换也变得自然无缝。比如说car里面一个num属性需要进行配置:

var Car = function() {
  this.num = null;
}

Car.prototype.run = function() {
  console.log('run car' + this.num);
  return 'car' + this.num;
}

module.exports = Car;

在context.json中,可以配置 num 为一个 ${car.num} 的占位符:

{
  "name": "simple",
  "beans": [{
    "id": "car",
    "func": "car",
    "props": [{
      "name": "num",
      "value": "${car.num}"
    }]
  }]
}

${car.num} 占位符最终会被特定环境下的值所替代。在config文件夹下面,对不同环境分不同的子目录,开发环境对应于dev,生产环境对应于prod,里面有car.num具体的配置:

├─┬ placeholderSample/
│ ├─┬ config/
│ │ └─┬ dev/
│ │ │ └── car.json
│ │ └─┬ prod/
│ │   └── car.json
│ └── car.js
└── context.json
{
    "car.num": 100
}

通过启动参数指定env的值来部署到不同的环境中。部署到生产环境中的示例如下:

node app.js env=prod

总结

本文中深入介绍了IoC在Node.js中的应用以及所给Node.js开发带来的便捷与好处。IoC可以去除代码之间的直接依赖关系,降低了耦合性。通过灵活可配置可重用的元数据配置,开发者在进行开发的时候面对的就不仅仅是一个个对象个体,而是弹性可配置的整体。IoC同时使得根据环境进行配置变得简单与无缝。

参考资料

  • bearcat 一个基于POJOs的应用层框架,提供了IoC、AOP、一致性配置等特性
  • bearcat-IoC容器详解 bearcat IoC容器各种特性详细介绍

感谢田永强对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

ioc好处 by fni fantasyni

有网友在微博上说js动态非常灵活,其实不需要ioc,这点见仁见智把,牛人还是有的,但是ioc最大的好处其实就是把所有的js对象都统一管理起来了,之后实现比如AOP(代码的trace追踪),代码热更新都非常方便,否则像代码热更新在node中很难做到(要处理依赖引用关系?)

好处 by hai piao

IOC对资源的管理和回收控制却实是一大优势

解耦好 by z lasia

用Ioc做解耦还是挺好的,特别是在测试的时候。

node spring by peng yang

在web应用开发方面还是很有优势,相当不错的框架

测试挺好 by Xy Wang

确实用这个做测试挺好,比较容易构建mock

MOCK测试 by G Gloria

让mock测试变得简单,确实是一大特色。不过对于IoC存在的一些缺陷也是需要有效地避开使用的。

如果像 AngularJS 那样用参数名做声明是否更好? by Chen Ken

这样用另一个文件来声明,其实和 Spring 一开始的 XML declaration 差不多,后来都转用 Java Annotation 加包扫描声明。感觉还是贴近代码好。

Re: 如果像 AngularJS 那样用参数名做声明是否更好? by 刘 松

花了一分钟滚过了文章正文,赞同Chen Ken的意见。
IoC解决的是什么问题,在nodejs的应用场景下是否还有这种问题,以什么形态出现?
明确些,相比拥有死板类型系统的传统静态语言,js这种动态语言是不是还需要IoC,都值得考虑。

angularjs的IoC侧重点是根据函数声明里的参数名,注入同名的服务。
如何得到一个function声明时的参数名?
angularjs的做法是,对function对象toString后,用正则进行字符串分析,土,但做到要的效果了。

Re: 如果像 AngularJS 那样用参数名做声明是否更好? by fni fantasyni

恩,确实,我们会考虑尽快支持的哈

ioc by 许 文敏

IOC框架不错

Re: 如果像 AngularJS 那样用参数名做声明是否更好? by fni fantasyni

bearcat 最新版本已经支持 "$命名的变量注入" "http://nodejs.netease.com/topic/5395817bbf23dba73410b031"

JS不需要IoC by 廖 雪峰

从Java搞出来的IoC根本不适合JavaScript。JS的优势是函数式编程,应该尽量使用函数,而不是new出一堆类来

mark by tim huang

mark 一下,下来仔细做个研究

Re: JS不需要IoC by fni fantasyni

没有new出对象来啊,js 是函数式和面向对象结合吧,如果纯用函数式估计没一个项目是这样的

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

14 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT