11 月 19 - 20 日 Apache Pulsar 社区年度盛会来啦,立即报名! 了解详情
写点什么

当 IoC 遇见了 Node.js

  • 2014-06-04
  • 本文字数:4966 字

    阅读完需:约 16 分钟

没有 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 同时使得根据环境进行配置变得简单与无缝。

参考资料


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

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

2014-06-04 23:2515993

评论

发布
暂无评论
发现更多内容

云安全系列2:访问安全和身份管理

HummerCloud

云计算 云安全 iam 身份和访问管理 10月月更

人人能读懂redux原理剖析

夏天的味道123

React

python计算机二级 常用函数操作

Python-派大星

10月月更

那些你不知道的炫酷导航交互效果

南城FE

CSS 前端 交互设计 导航 交互

Vue虚拟dom是如何被创建的

yyds2026

Vue

Vue模板是怎样编译的

yyds2026

Vue

引擎上新|卡片焕新升级,信息高效呈现

Jianmu

DevOps 持续集成 CI/CD

线上数据问题排查案例分享-因为 HMS 和底层 orc 文件中某字段的数据精度不一致造成的数据丢失问题

明哥的IT随笔

hadoop hive DataX

深入nodejs的event-loop

coder2028

node.js

SAP | 子例程

暮春零贰

SAP 10月月更 子例程

漏洞评分高达9.8分!Text4Shell 会是下一个 Log4Shell吗?

SEAL软件供应链安全

安全 log4j 漏洞分析 Log4j2 漏洞 软件供应链安全

Java:既然有了synchronized,为什么还要提供Lock

华为云开发者联盟

Java 开发 华为云 企业号十月 PK 榜

5 why 分析法,一种用于归纳抽象出解决方案的好方法

SaaS创业之路

【沙丘大会】九科信息研发中心自动化负责人郑文茂受邀分享央企数字员工实践案例

九科Ninetech

redux原理是什么

xiaofeng

React

云小课|MRS基础原理之Oozie任务调度

华为云开发者联盟

大数据 华为云 企业号十月 PK 榜

【1024程序员节专访】聚焦行业前沿,共话IT发展趋势

博睿数据

程序员 可观测性 智能运维 博睿数据 IT行业

云计算基础:云计算运用越来越广泛,我们应该如何去学习云计算

Python-派大星

10月月更

一文读透react精髓

xiaofeng

React

京东云开发者|ElasticSearch降本增效常见的方法

京东科技开发者

elasticsearch ES 降本增效 数据压缩 存储计算分离

彻底搞懂nodejs事件循环

coder2028

node.js

Docker进阶 dockerfile指令构建docker镜像

Python-派大星

10月月更

Workflow,要不要了解一下

华为云开发者联盟

人工智能 华为云 企业号十月 PK 榜

webpack模块化的原理

Geek_02d948

webpack

原生拖拽太拉跨了,纯JS自己手写一个拖拽效果,纵享丝滑

茶无味的一天

CSS html HTML5, CSS3 拖拉拽 原生js

长安链源码分析同步服务器1

小样本学习在文心ERNIE3.0多分类任务应用--提示学习

汀丶

nlp 文本分类

Vue组件是怎样挂载的

yyds2026

Vue

webpack实战,手写loader和plugin

Geek_02d948

webpack

Webpack配置实战

Geek_02d948

webpack

【文本检测与识别白皮书-3.2】第二节:场景文本识别方法

合合技术团队

人工智能 深度学习 文字识别 OCR 文本识别

当IoC遇见了Node.js_Node.js_倪震洋_InfoQ精选文章