手把手教你实现TypeScript下的IoC容器
在此篇文章开始之前,先向大家简单介绍 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容器相关推荐
- 手把手教你在Windows下使用MinGW编译libav
2019独角兽企业重金招聘Python工程师标准>>> 手把手教你在Windows下使用MinGW编译libav libav是在Linux下使用纯c语言开发的,不可避免的,libav ...
- 手把手教你在Windows下使用MinGW编译libav(参考libx264的编入)
转自:http://www.th7.cn/Program/cp/201407/242762.shtml 手把手教你在Windows下使用MinGW编译libav libav是在Linux下使用纯c语言 ...
- centos7重新加载服务的命令_阿粉手把手教你在 CentOS7 下搭建 Jenkins
每天早上七点三十,准时推送干货 阿粉的公司是用 Jenkins 去做的 DevOps 实践,那么想要快速熟悉 Jenkins ,第一步就是去把它搭建一下,这周末闲着没事就玩了一把,将整个过程和大家分享 ...
- 手把手教你在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 ...
- linux按照mysql为何如此简单_手把手教你在Linux下安装MySQL
在Linux操作系统下,安装MYSQL有两种方式:一种tar安装方式,另外一种是rpm安装方式.这两种安装方式有什么区别呢?尽管我们在Linux下常用tar来压缩/解压缩文件,但MYSQL的tar格式 ...
- 手把手教如何用Linux下IIO设备(附代码)
关注.星标嵌入式客栈,精彩及时送达 [导读] 朋友们,大家好,我是逸珺. 今天分享一下如何在用户空间操作IIO设备.IIO设备能实现很多有价值的应用,有兴趣的一起来看看~ 什么是IIO设备 IIO是 ...
- 手把手教你在windows下源码编译Open3D
文章目录 前言 1.编译环境 2.编译步骤 3.编译中的bug 3.1 下载超时问题,ispc.pybind11.open3d_sphinx_theme等 3.2 boringssl 3.3 Dire ...
- 手把手教你在Windows下安装miniconda及常用命令
安装 官网 下载安装包 选择合适的安装包,我打开官网的时候默认是python2.7和python3.8的,由于部分依赖暂不支持python3.8,我这里想安装python3.7的 查看全部版本:htt ...
- 手把手教你配置linux下C++开发工具——vim+ycm(YouCompleteMe),支持基于语义的自动补全和第三方库补全(史上最简单、史上最透彻、史上最全的终极解决方案)
截止到目前,vim稳定版本已经到了8.2+,ycm(YouCompleteMe的简称)最新版本与几年前的安装配置截然不同了.之前网上很多教程也教不得法,生搬硬套,没有讲透彻.所以,才下定决心写一篇自认 ...
最新文章
- 安装GitLab,Jenkins,及自动化上线
- Asp.net SignalR快速入门 ---- /signalr/hubs 404
- c++ assert()断言
- Mysql函数示例(如何定义输入变量与返回值)
- 史上最全MySQL 大表优化方案(长文)
- 59-混沌操作法感悟2.(2015.2.25)
- 时间同步失败_关于同步、异常处理的思考
- 数据分析实战之自如房租分析
- linux根目录被mv,【Linux】mv根目录的恢复(转)
- Ruby 28 岁生日快乐!
- python的plot如何实时更新中_python中plot实现即时数据动态显示方法
- hdu 5187 zhx's contest
- 弹性法计算方法的mck法_经济学原理中讲到的中点法计算需求弹性是怎么回事
- 腾讯云 鉴权失败,请确认服务器已启用密码鉴权并且账号密码正确? permission denied (publickey,gssapi-keyex,gssapi-with-mic)
- 计算机网络10--路由冗余备份,缺省(静态)路由配置
- python爬虫---拉勾网与前程无忧网招聘数据获取(多线程,数据库,反爬虫应对)
- 苹果HomeKit生态深度解析,在智能家居领域后发制人?
- 【备忘】linux视频
- 如何彻底关闭win11自动更新
- 乘2取整法_十进制小数转二进制小数乘2取整法的直观理解