在此篇文章开始之前,先向大家简单介绍 IoC。什么是 IoC?以及为什么我们需要 IoC?以及本文核心,在 TypeScript 中实现一个简单的 IoC 容器?

目录 [隐藏]

  • IoC 定义
  • 初识 Container
  • 原理揭秘
    • @inject
    • Container
  • 小结

IoC 定义

我们看维基百科定义:

控制反转(Inversion of Control,缩写为 IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称 DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递(注入)给它。 ———— 维基百科

简单来说,IoC 本质上是一种设计思想,可以将对象控制的所有权交给容器。由容器注入依赖到指定对象中。由此实现对象依赖解耦。

初识 Container

假设我们有三个接口: Warrior 战士、Weapon 武器、ThrowableWeapon 投掷武器。

export interface Warrior {fight(): string;sneak(): string;
}
export interface Weapon {hit(): string;
}
export interface ThrowableWeapon {throw(): string;
}

对应分别有实现这三个接口的类:Katana 武士刀、Shuriken 手里剑、以及 Ninja 忍者。

export class Katana implements Weapon {public hit() {return 'cut!';}
}
export class Shuriken implements ThrowableWeapon {public throw() {return 'hit!';}
}
export class Ninja implements Warrior {private _katana: Weapon;private _shuriken: ThrowableWeapon;public constructor() {this._katana = new Katana();this._shuriken = new Shuriken();}public fight() {return this._katana.hit();}public sneak() {return this._shuriken.throw();}
}

由上面的示例,很明显我们可以得知,Ninja 类依赖了 Katana 类和 Shuriken 类。这种依赖关系对于我们来说很常见,但是随着应用的日益迭代,越来越复杂的情况下,类与类之间的耦合度也会越来越高,应用会变得越来越难以维护。

对于上述 Ninja 类来说,如若日后需要不断新增其他武器对象,甚至忍术对象,这个 Ninja 类文件会引入越来越多的对象,Ninja 类也会越来越臃肿。如果一个应用内部每一个类都对彼此产生依赖,可能代码写到后面就是沉重的技术债了。

因此 IoC 的思想的出现,就是为了实现对象依赖解耦。

那么先带大家简单认识 IoC 容器的使用。

const container = new Container();
const TYPES = {Warrior: Symbol.for('Warrior'),Weapon: Symbol.for('Weapon'),ThrowableWeapon: Symbol.for('ThrowableWeapon')
};
container.bind<Weapon>(TYPES.Weapon).to(Katana);
container.bind<ThrowableWeapon>(TYPES.ThrowableWeapon).to(Shuriken);
container.bind<Warrior>(TYPES.Warrior).to(Ninja);
const ninja = container.get<Ninja>(TYPES.Warrior);
ninja.fight(); // "cut!"
ninja.sneak(); // "hit!"

上面的 Container 实际上接手了对象依赖的管理,使得 Ninja 类脱离了对 Katana 类和 Shuriken 类的依赖!

此时 Ninja 类只依赖抽象的接口(Weapon、ThrowableWeapon)而不是依赖具体的类(Katana、Shuriken)。

原理揭秘

那么 Container 怎样做到的呢?它的实现原理又是怎样的呢?是不是很好奇?其实没有什么黑魔法,接下来就会为大家揭开 Container 实现 IoC 原理的神秘面纱。

首先我们先将 Ninja 类改写如下:

export class Ninja implements Warrior {private _katana: Weapon;private _shuriken: ThrowableWeapon;public constructor(@inject(TYPES.Weapon) katana: Weapon, @inject(TYPES.ThrowableWeapon) shuriken: ThrowableWeapon) {this._katana = katana;this._shuriken = shuriken;}public fight() {return this._katana.hit();}public sneak() {return this._shuriken.throw();}
}

@inject

可以发现。我们在 Ninja 类的构造函数里对每个参数进行了 @inject 装饰器声明。那么这个@inject 又干了什么事情?。@inject 也不过是我们实现的一个装饰器函数而已,代码如下:

export function inject(serviceIdentifier: string | symbol) {return function(target: any, propertyKey: string, parameterIndex: number) {const metadata = {key: 'inject.tag',value: serviceIdentifier};Reflect.defineMetadata(`custom:paramtypes#${parameterIndex}`, metadata, target);};
}

这里出现了 Reflect.defineMetadata,大家可能比较陌生。Reflect Metadata 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。

提案文档: Metadata Proposal – ECMAScript。

想要使用此特性,需要安装 reflect-metadata 这个包,同时配置 tsconfig 如下:

{"compilerOptions": {"target": "es5","lib": ["es6", "DOM"],"types": ["reflect-metadata"],"module": "commonjs","moduleResolution": "node","experimentalDecorators": true,"emitDecoratorMetadata": true}
}

在这个场景下,我为 @inject 对象里每个传入的参数自定义了 metadataKey。比如在上述@inject(TYPES.Weapon)中,target 就是 Ninja 类,parameterIndex 就是 0。@inject(TYPES.ThrowableWeapon)同样道理。

因此在 Ninja 类里,根据@inject 装饰器的声明,在运行时给 Ninja 类添加了两个元数据。

custom:paramtypes#0 -> { key: "inject.tag", value: TYPES.Weapon }
custom:paramtypes#1 -> { key: "inject.tag", value: TYPES.ThrowableWeapon }

Container

IoC 容器的主要功能是什么呢?

  • 类的实例化
  • 查找对象的依赖关系

以下是一个十分简单的 Container 容器实现代码。

type Constructor<T = any> = new (...args: any[]) => T;
class Container {bindTags = {};bind<T>(tag: string | symbol) {return {to: (bindTarget: Constructor<T>) => {this.bindTags[tag] = bindTarget;}};}get<T>(tag: string | symbol): T {const target = this.bindTags[tag];const providers = [];for (let i = 0; i < target.length; i++) {const paramtypes = Reflect.getMetadata('custom:paramtypes#' + i, target);const provider = this.bindTags[paramtypes.value];providers.push(provider);}return new target(...providers.map(provider => new provider()));}
}

bind 方法,主要将所有绑定在容器上依赖建立映射关系。比如以下代码:

const container = new Container();
container.bind<Weapon>(TYPES.Weapon).to(Katana);
container.bind<ThrowableWeapon>(TYPES.ThrowableWeapon).to(Shuriken);
container.bind<Warrior>(TYPES.Warrior).to(Ninja);

创建容器后,通过 bind 绑定了三个对象,因此容器中形成了(bindTags)以下这样的关系。

{[TYPES.Weapon]: Katana,[TYPES.ThrowableWeapon]: Shuriken,[TYPES.Warrior]: Ninja
}

绑定依赖对象后,我们再结合实例看容器的 get 方法:

const ninja = container.get<Ninja>(TYPES.Warrior);

容器的 get 方法通过 tag 参数在 bingTags 映射里,找到目标对象,对应到上述代码也就是,找到了 Ninja 类。

紧接着重头戏来了,我们可以通过 target.length(也就是 function.length)得知 Ninja 类构造函数的参数数量,声明了 providers 数组用于存储 Ninja 类的依赖。还记得一开始我们通过 @inject 在类上添加的两个元数据。此时发挥了重要作用!因此通过元数据即可查找到依赖。

如下:

const paramtypes = Reflect.getMetadata('custom:paramtypes#' + i, target);
const provider = this.bindTags[paramtypes.value];

第一个参数对应 custom:paramtypes#0,paramtypes.value 即为 TYPES.Weapon,此时在 bindTags 查到,找到了 Katana 类依赖!

同理第二个参数也找到了 Shuriken 类依赖。

找到所有在构造函数中声明的依赖后,真正开始注入依赖,如下。

return new target(...providers.map(provider => new provider()));

因此最后,通过容器 get 方法,成功得到了注入了依赖的 ninja 实例。

ninja.fight(); // "cut!"
ninja.sneak(); // "hit!"

噌噌噌!正确运行!

小结

通过容器管理,类真正做到了依赖抽象的接口,而不是依赖具体的类。践行了 IoC 的思想。

不过上文的 IoC 容器,也只是一个小小的玩具,它所产生的意义主要是引导指示的价值。希望通过此文,可以让大家理解和重视 IoC 的使用。当然笔者也是刚刚学习 IoC,业余时间敲下这个 demo,自己的乐趣和收获也很多~

以上,对大家如有助益,不胜荣幸。

https://juejin.im/post/5e167abc6fb9a047f66ebb7c

转自https://www.lizenghai.com/archives/43943.html

手把手教你实现TypeScript下的IoC容器相关推荐

  1. 手把手教你在Windows下使用MinGW编译libav

    2019独角兽企业重金招聘Python工程师标准>>> 手把手教你在Windows下使用MinGW编译libav libav是在Linux下使用纯c语言开发的,不可避免的,libav ...

  2. 手把手教你在Windows下使用MinGW编译libav(参考libx264的编入)

    转自:http://www.th7.cn/Program/cp/201407/242762.shtml 手把手教你在Windows下使用MinGW编译libav libav是在Linux下使用纯c语言 ...

  3. centos7重新加载服务的命令_阿粉手把手教你在 CentOS7 下搭建 Jenkins

    每天早上七点三十,准时推送干货 阿粉的公司是用 Jenkins 去做的 DevOps 实践,那么想要快速熟悉 Jenkins ,第一步就是去把它搭建一下,这周末闲着没事就玩了一把,将整个过程和大家分享 ...

  4. 手把手教你在windows10下进行openFoam调试

    参考 :http://www.xfy-learning.com/2021/01/05/%E5%88%A9%E7%94%A8VS-Code%E9%98%85%E8%AF%BB%E6%BA%90%E7%A ...

  5. linux按照mysql为何如此简单_手把手教你在Linux下安装MySQL

    在Linux操作系统下,安装MYSQL有两种方式:一种tar安装方式,另外一种是rpm安装方式.这两种安装方式有什么区别呢?尽管我们在Linux下常用tar来压缩/解压缩文件,但MYSQL的tar格式 ...

  6. 手把手教如何用Linux下IIO设备(附代码)

    关注.星标嵌入式客栈,精彩及时送达 [导读] 朋友们,大家好,我是逸珺. 今天分享一下如何在用户空间操作IIO设备.IIO设备能实现很多有价值的应用,有兴趣的一起来看看~ 什么是IIO设备 IIO是 ...

  7. 手把手教你在windows下源码编译Open3D

    文章目录 前言 1.编译环境 2.编译步骤 3.编译中的bug 3.1 下载超时问题,ispc.pybind11.open3d_sphinx_theme等 3.2 boringssl 3.3 Dire ...

  8. 手把手教你在Windows下安装miniconda及常用命令

    安装 官网 下载安装包 选择合适的安装包,我打开官网的时候默认是python2.7和python3.8的,由于部分依赖暂不支持python3.8,我这里想安装python3.7的 查看全部版本:htt ...

  9. 手把手教你配置linux下C++开发工具——vim+ycm(YouCompleteMe),支持基于语义的自动补全和第三方库补全(史上最简单、史上最透彻、史上最全的终极解决方案)

    截止到目前,vim稳定版本已经到了8.2+,ycm(YouCompleteMe的简称)最新版本与几年前的安装配置截然不同了.之前网上很多教程也教不得法,生搬硬套,没有讲透彻.所以,才下定决心写一篇自认 ...

最新文章

  1. 安装GitLab,Jenkins,及自动化上线
  2. Asp.net SignalR快速入门 ---- /signalr/hubs 404
  3. c++ assert()断言
  4. Mysql函数示例(如何定义输入变量与返回值)
  5. 史上最全MySQL 大表优化方案(长文)
  6. 59-混沌操作法感悟2.(2015.2.25)
  7. 时间同步失败_关于同步、异常处理的思考
  8. 数据分析实战之自如房租分析
  9. linux根目录被mv,【Linux】mv根目录的恢复(转)
  10. Ruby 28 岁生日快乐!
  11. python的plot如何实时更新中_python中plot实现即时数据动态显示方法
  12. hdu 5187 zhx's contest
  13. 弹性法计算方法的mck法_经济学原理中讲到的中点法计算需求弹性是怎么回事
  14. 腾讯云 鉴权失败,请确认服务器已启用密码鉴权并且账号密码正确? permission denied (publickey,gssapi-keyex,gssapi-with-mic)
  15. 计算机网络10--路由冗余备份,缺省(静态)路由配置
  16. python爬虫---拉勾网与前程无忧网招聘数据获取(多线程,数据库,反爬虫应对)
  17. 苹果HomeKit生态深度解析,在智能家居领域后发制人?
  18. 【备忘】linux视频
  19. 如何彻底关闭win11自动更新
  20. 乘2取整法_十进制小数转二进制小数乘2取整法的直观理解

热门文章

  1. 随机过程在计算机领域的应用,清华大学出版社-图书详情-《随机过程及其在金融领域中的应用(第2版)》...
  2. 碱性干电池的内阻测试方法_实测南孚一号干电池内阻
  3. python爬虫 关于加速乐(_jsl)
  4. docker-compose部署MinIO分布式集群
  5. 浙大计算机学院多厉害,一张图,就能告诉你浙大到底有多牛!
  6. rabbitmq关于delivery_tag
  7. 英语国际音标教学视频
  8. turfjs前端地理空间分析类库
  9. Linux使用基础(目录)顶顶顶
  10. 简单使用github上的节操播放器