在这个例子中我们直接导入了 db 模块,因此这个文件就依赖于 db 模块在磁盘上的具体存储位置,以及依赖于特定的是方式。在大多数场景下这并不算一个大问题。不过这种方式让测试变得更加困难 -- 不至于无法进行测试,但是无论如何都变得更加地困难了。另外,这个模块还假定 db 模块已经准备好了(例如:数据库连接已经建立起来了)。
如果我们进一步将上面的代码转化成为对于测试友好的 DI 实现方式:
1 2 3 4 5 6 7
exportdefaultfunctionmakeTodosService({ db }) { return { getTodos: () => { return db.query("select * from todos"); }, }; }
那么上面两个例子有什么区别呢?在下面的 DI 实现的例子中我们不是 export 出一个对象,而是 export 出一个生成这种对象的函数。这个函数同时阐明了为了创建此种对象所需要的依赖。
如果你熟悉在其他语言中的 DI 实现,如 Java, C#,还有 PHP。下面这个使用 ES6 的类实现的例子可能更受你喜欢一些:
1 2 3 4 5 6 7 8
exportdefaultclassTodosService { constructor({ db }) { this.db = db; } getTodos() { returnthis.db.query("select * from todos"); } }
不过从个人角度我还是更喜欢函数的方法:不用担心 this 的上下文的问题。
测试上面这个基于 DI 的例子非常简单 -- 你不再需要担心对 require 进行修修补补来替代数据库模块从而连接到测试数据库。
// Using object destructring to make it look good exportfunctionmakeTodosService({ // "repository" is a fancy term to describe an object // that is used to retrieve data from a datasource - the actual // data source does not matter. Could be a database, a REST API, // or some IoT things like sensors or what ever todosRepository, // We also want info about the user that is using the service, // so we can restrict access to only their own todos. currentUser, }) { assert(todosRepositry, "opts.todosRepository is required."); assert(currentUser, "opts.currentUser is required."); return { // Gets todos for the current user getTodos: async (query) => { const todos = await todosRepository.find({ // can be ALL, INCOMPLETED, COMPLETED filter: query.filter, userId: currentUser.id, }); return todos; }, createTodo: async (data) => { const newTodo = await todosRepository.create({ text: data.text, userId: currentUser.id, completed: false, }); return newTodo; },
describe('Todos System', function () { it('works', asyncfunction() { // This is how DI is done manually const todosService = makeTodosService({ todosRepository: newTodosRepository(), // Let's fake it til we make it! currentUser: { id: 123, name: 'Jeff' } })
// Todos Service already knows who's creating it! const created = await todosService.create({ text: 'Write Medium article' }) expect(created.userId).to.equal(123, 'user id should match currentUser')
const todos = await todosService.getTodos({ filter: 'ALL' }) expect(todos.length).to.equal(1)
Angular 曾经是在 JavaScript 世界中第一个引入了 DI 的大型框架。他们的做法是使用函数的字符串表达来提取使用的模块名称。在当时这是唯一的做法。
有一些人尝试将 DI 功能从 Angular 中独立出来做成一个独立模块。但是问题是,大多数 DI 模块要求你的所有代码都要围绕着特定的 DI 系统来开发,这位违背了 DI 设计理念的初衷。
DI 的作用是减少程序模块之间的耦合程度,提高代码的可维护性。在这种目标下,DI 系统的设计应当尽可能减少对于其它业务代码的影响。如果为了使用 DI 要对业务代码结构进行大范围的改动的话就得不偿失了。
我们希望能够在不改动我们的 service 和 repository 模块的情况下使用 DI 机制。
3.1 关于 Awilix - The DI container you deservce
如果你不知道 DI 容器是什么,下面是一个简短的解释。DI 容器的功能是将系统中的模块整合起来,从而让开发者不再需要太关注这些 DI 的实现细节问题。在前面两个 Part 中我们给出的示例代码:实例化 services 和 repositories,确保 service 获取 repository 对象。这些工作都将由 DI 容器来完成。
// Ordering does not matter container.register({ // Notice the scoped() at the end - this signals // Awilix that we gonna want a new instance per "scope" todosService: asFunction(makeTodosService).scoped(), // We only want a single instance of this for the apps // lifetime (it does not deal with user context) // so we can reuse it! todosRepository: asClass(TodosRepository).singliton(), });
// This installs a scoped container into our // context - we will use this to register our current user app.use(scopePerRequest(container)) // Let's do that now! app.use((ctx, next) => { ctx.state.container.register(Value)({ // Imagine some auth middleware somewhere... // This makes currentUser available to all services currentUser: ctx.state.user }) returnnext() })
// Now our handlers will be able to resolve a todos service // using DI! // P.S: be a good dev and use multiple files. ;) const todosAPI = ({ todosService } => { return { getTodos: async (ctx) => { const todos = await todosService.getTodos(ctx.request.query) ctx.body = todos ctx.status = 200 }, createTodos: async (ctx) => { const todo = await todosService.createTodo(ctx.request.body) ctx.body = todo ctx.status = 201 }, updateTodo: async (ctx) => { const updated = await todosService.updateTodo( ctx.params.id, ctx.request.body ) ctx.body = updated, ctx.status = 200 }, deleteTodo: async (ctx) => { await todosService.deleteTodo( ctx.params.id, ctx.request.body ) } } })
// Awilix magic will run the above function // every time a request comes in, so we have // a set of scoped services per request const api = makeInvoker(todosAPI) router.get('/todos', api('getTodos')) router.post('/todos', api('createTodos')) router.patch('/todos/:id', api('updateTodo')) router.patch('/todos/:id', api('deleteTodo'))
app.use(router.routes()) app.listen(1337)
上面的代码还只是一个简单的雏形,不过你现在已经有了构建大规模项目的基础。
3.2 结论
DI 是一个很有用的东西,不过手动去实现 DI 是一件糟心的事情。这也是 Awilix 这种 DI 容器扮演作用的地方。