要知道,业界 Java 体系的 Spring Framework 已经成为标配,越是庞大的项目,越是需要 IoC 容器的支持。如果你还不了解什么是依赖注入(DI),什么是 IoC,那么,也没关系,我们这就手把手教你。
首先,我们先理清一下思路,我们不会实现一个非常复杂的东西,也不会出现前三步都是一笔画,而第四步就出现天籁美图,自然也不会教你杀一条龙(那是勇者的工作)。
简单的描述一下需求,我有多个类,只要能够通过一个简单的方法,获取类的实例就可以。
比如,我们只需要能实现下面的代码就算是完成了:
class A {
b: B;
}
class B {
c: C;
}
class C {
hello() {
console.log('hello world');
}
}
const container = new Container();
const a = container.get(A);
// a.b.c.hello() === 'hello world'
第一步
我们在上面的例子里看到了 Container 类,首先我们先得知道 Container 需要做什么。
1,2,3,没错,我们觉得它需要分析 A 这个类的属性中依赖了哪些东西,我们一眼就能看出 A 依赖了 B,而 B 依赖了 C,但是程序呢?
为了简化一下,这个地方需要有一个假设,假如我们的属性都是标准的类好了,现在就简单了。
class Container {
get(Module) {
// 创建对象
const obj = new Module();
// 拿到属性
const properties = Object.getOwnPropertyNames(obj);
for(let p of properties) {
if(!obj[p]) {
// 如果对象不存在,就往下创建
if(p === 'b') {
obj[p] = this.get(B);
} else if(p === 'c') {
obj[p] = this.get(C);
} else {}
}
}
}
}
我们使用了递归的方式来不断的 get 某一个类的实例,碰到如果对象不存在,则不断的进行创建,到这里看起来基本就完成了呢,拍手。
第二步
好在我们只有 3 个类,核心功能这么快就完成了呢,下一步我们来想想怎么优化吧。
如果有另一个类,包裹了其他的几个实例怎么办呢。
class A {
b: B;
d: D;
}
class B {
c: C;
}
class D {
b: B;
c: C;
}
按照我们的核心代码,估计就会创建出好几个 B 实例和 C 实例啦。
那么,开始优化吧,我们先加个缓存吧。
class Container {
cache = {};
getName(Module) {
return Module.name.toLowerCase();
}
get(Module) {
// 弄个缓存
if(this.cache[this.getName(Module)]) {
return this.cache[this.getName(Module)];
}
// 创建对象
const obj = new Module();
// 缓存起来下次用
this.cache[this.getName(Module)] = obj;
// 拿到属性
const properties = Object.getOwnPropertyNames(obj);
for(let p of properties) {
if(!obj[p]) {
// 如果对象不存在,就往下创建
if(p === 'b') {
obj[p] = this.get(B);
} else if(p === 'c') {
obj[p] = this.get(C);
} else if(p === 'd') {
obj[p] = this.get(D);
} else {}
}
}
}
}
经过了缓存,我们可以保证在一次运行中,每个类都只会有一个对象实例存在啦。
好学的第三步
本想就这么结束了,但是好学的同学总是太多,那么再做点微小的工作吧。
我们现在只能创建 A,B,C,D 这四个类,是不是有点不太够?
没关系,我们这就教你怎么样支持任意的类。
聪明的人类想出了一个方法,如果我提前就知道了有哪些类,存在一个表(Map)里,下次用的时候如果名字一样就能匹配上了呢。
Bingo,就是这样。
init() {
const fileResults = globby.sync(['**/**.ts', '**/**.js'], {
cwd: process.cwd(),
ignore: [
'**/node_modules/**',
],
});
for (const name of fileResults) {
const exports = require(process.cwd() + '/' + name);
// 把类名和类都存起来
this.classTable[this.getName(exports)] = exports;
}
}
这里使用了一个扫描机制,在执行初始化的时候把类的信息通通放到 classTable
下面,之后我们就能直接使用啦,而且不用关心类名叫什么,把 26 个字母随意组合都可以。
const globby = require('globby');
const path = require('path');
class Container {
cwd = process.cwd();
cache = {};
classTable = {};
init() {
const fileResults = globby.sync(['**/**.ts', '**/**.js'], {
cwd: this.cwd,
ignore: [
'**/node_modules/**',
],
});
for (const name of fileResults) {
const exports = require(this.cwd + '/' + name);
// 把类名和类都存起来
this.classTable[this.getName(exports)] = exports;
}
}
getName(Module) {
return Module.name.toLowerCase();
}
get(Module) {
// 弄个缓存
if(this.cache[this.getName(Module)]) {
return this.cache[this.getName(Module)];
}
// 创建对象
const obj = new Module();
// 缓存起来下次用
this.cache[this.getName(Module)] = obj;
// 拿到属性
const properties = Object.getOwnPropertyNames(obj);
for(let p of properties) {
// 如果对象不存在,就往下创建
if(!obj[p]) {
// 如果表里有,就创建
if(this.classTable[p]) {
obj[p] = this.get(this.classTable[p]);
}
}
}
}
}
完成的最后
恭喜大家毕业啦,以后面试你就可以说,我会写 IoC 容器,妥妥的稳过啊。
当然实际的情况会复杂很多,上面的代码只是 injection
包中的核心的精简和抽象,如果遇到复杂场景还是推荐大家直接使用 injection
包,不过想了想应该机会不多,想用 IoC 开发应用,体验依赖注入和应用解耦。直接用我们的 midway
就好了嘛。
老板说了,点下 好看
可以加鸡腿哦。