第三篇:IoC初探
这是使用TypeScript一步一步实现IoC系列的第三篇文章.
我们知道,使用IoC模式时,需要把创建依赖的责任就给上层,上层进行统一的管理,那么,上层是什么呢?这里的上层就是我们通常所说的IoC container.
这篇文章中我们将实现一个简单的IoC container.
一.创建一个测试类文件
1 export class Hand { 2 public hit() { 3 return 'CUT!'; 4 } 5 } 6 7 export class Mouth { 8 public bite() { 9 return 'Hit!'; 10 } 11 } 12 13 export class Human { 14 private hand: Hand; 15 private mouth: Mouth; 16 public constructor( 17 hand: Hand, 18 mouth: Mouth 19 ) { 20 this.hand = hand; 21 this.mouth = mouth; 22 } 23 public fight() { 24 return this.hand.hit(); 25 } 26 public sneak() { 27 return this.mouth.bite(); 28 } 29 }
可以看到,这里的Human来中的constructor有2个参数,分别是Hand类和Mouth类的实例,这里没有在constructor中实例化Hand类和Mouth类,而是把实例的任务交给上层,就是使用IoC的模式。
那么,这个上层是哪里呢?我们来简单的应用一下DI
二.创建Container文件
1 import { Hand, Mouth } from "./test-class"; 2 3 type TagKey = string; 4 type TagValue = string | Function; 5 6 export class Container { 7 private bindTags: any = {}; 8 public bind(tag: TagKey, value: TagValue) { 9 this.bindTags[tag] = value; 10 } 11 public get<T>(tag: TagKey): T { 12 const target = this.bindTags[tag]; 13 if (target) { 14 throw new Error("Can not find the provider"); 15 } 16 return new target(new Hand(), new Mouth()) 17 } 18 }
Container类中定义了bind方法和get方法,bind方法用来绑定键与依赖的关系,get方法通过键来找到依赖,返回给调用方。
三.使用
index.ts
1 import { Container } from "./container"; 2 import { Human } from "./test-class"; 3 4 const container = new Container(); 5 6 container.bind('Human', Human); 7 8 const human = container.get<Human>('Human'); 9 10 human.fight(); 11 human.sneak();
代码正常运行,可以看到,这里在获取human实例时,我们没有自己去创建Human类的依赖,也就是Hand类和Mouth类,这就实现了Human类与Hand类,Mouth类的不依赖。
四.自动识别Hand类和Mouth类
如果详细的看一下,就会发现Container来写的如此的丑陋,我们一步步来优化它。
首先,Container中的get函数肯定是不能直接使用Hand类和Mouth类的,那么,怎么获取呢,能不能也像Human类一样使用bind函数来绑定呢?下面我们来实现一下。
1 import 'reflect-metadata'; 2 import { Hand, Mouth } from './test-class'; 3 4 type Tag = string; 5 type Constructor<T = any> = new (...args: any[]) => T; 6 type BindValue = string | Function | Constructor<any>; 7 8 9 10 export class Container { 11 private bindTags: any = {}; 12 public bind(tag: Tag, value: BindValue) { 13 this.bindTags[tag] = value; 14 } 15 public get<T>(tag: Tag): T { 16 const target = this.getTagValue(tag) as Constructor; 17 const providers: BindValue[] = []; 18 for(let i = 0; i < target.length; i++) { 19 // 获取参数的名称 20 const providerKey = Reflect.getMetadata(`design:paramtypes`, target)[i].name; 21 // 把参数的名称作为Tag去取得对应的类 22 const provider = this.getTagValue(providerKey); 23 providers.push(provider); 24 } 25 return new target(new Hand(), new Mouth()); 26 // return new target(...providers.map(p => new (p as Constructor)())) 27 } 28 private getTagValue(tag: Tag): BindValue { 29 const target = this.bindTags[tag]; 30 if (!target) { 31 throw new Error("Can not find the provider"); 32 } 33 return target; 34 } 35 }
可以看到,在get函数中通过了Reflect API获取到了constructor函数的参数名称,再把参数名称作为token去获取provider,也就是对应的类,
当然,可以这样做的前提是需要把类名称与类进行bind
1 import 'reflect-metadata'; 2 3 import { Container } from "./container"; 4 import { Hand, Human, Mouth } from "./test-class"; 5 6 const container = new Container(); 7 container.bind('Hand', Hand); 8 container.bind('Mouth', Mouth); 9 container.bind('Human', Human); 10 11 const human = container.get<Human>('Human'); 12 13 human.fight(); 14 human.sneak();
如果就这样执行代码的会,会报错,那是因为使用Reflect.getMetadata(`design:paramtypes`, target)时,有一些条件。
首先,MetaData这个题案还没有实现,我们需要导入外部模块reflect-metadata,即npm install reflect-metadata --save
另外,design:paramtypes这个元数据是需要在装饰器中才能被自动加入的,所以,我们需要给Hand类,Human类,Mouth类加上装饰器
1 export const injectable = (constructor: Object) => { 2 3 } 4 5 @injectable 6 export class Hand { 7 public hit() { 8 console.log('Cust!'); 9 return 'Cut!'; 10 } 11 } 12 @injectable 13 export class Mouth { 14 public bite() { 15 console.log('Hit!'); 16 return 'Hit!'; 17 } 18 } 19 @injectable 20 export class Human { 21 private hand: Hand; 22 private mouth: Mouth; 23 constructor( 24 hand: Hand, 25 mouth: Mouth 26 ) { 27 this.hand = hand; 28 this.mouth = mouth; 29 } 30 public fight() { 31 return this.hand.hit(); 32 } 33 public sneak() { 34 return this.mouth.bite(); 35 } 36 }
这样,虽然Injectable装饰器是空的,但是运用装饰器时reflect-metadata就会默认到给对象加上以下3个元数据:
design:type
: 属性类型design:paramtypes
: 参数类型design:returntype
: 返回值类型
至此,我们实现了简单的IoC,并且IoC Container与其他类完全的隔离。
下一步,我们会把bind这个步骤做成装饰器,这样更加的方便。