第三篇: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 }
test-class.ts

 可以看到,这里的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 }
simple container.ts

 

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 }
Container.ts

 

可以看到,在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这个步骤做成装饰器,这样更加的方便。

posted @ 2021-01-07 21:32  JasonWangTing  阅读(579)  评论(0编辑  收藏  举报