上一文《前端也要懂的解耦思想:从面向接口到IoC容器》我们遵循IoC设计思想手动实现了一个IoC容器(代码参考如下),利用IoC容器实现了模块间依赖关系的解耦。解耦的好处是显而易见的,但同时也带来了代码繁琐(模块需要手动注册进容器)的问题。而且高层模块需要手动从IoC容器获取依赖显得不够优雅,对逻辑有一定程度上的侵入。
interface TModule<T>{
new(...args: any[]): T
}
class Container{
private modules= new Map()
public register<T>(identifier: string, Module: TModule<T>, singleton: boolean = true, args: any[] = []){
this.modules.set(identifier, {
singleton,
args,
module: Module,
instance: null as T,
})
}
public get(identifier: string){
const moduleInfo= this.modules.get(identifier)
if(!moduleInfo.singleton || !moduleInfo.instance){
moduleInfo.instance= new moduleInfo.module(...moduleInfo.args)
}
return moduleInfo.instance
}
}
export const container= new Container()
interface TWeapon{ //定义需要实现的方法
start(victim: string) : void, //start方法只接受一个string类型参数,无返回内容
}
class Gun implements TWeapon{
private fire(victim: string){
console.log('gun is firing ' + victim)
}
public start(victim: string){
this.fire(victim)
}
}
container.register<Gun>('Gun', Gun)
class Knife implements TWeapon{
private cut(victim: string){
console.log('knife is cutting ' + victim)
}
public start(victim: string){
this.cut(victim)
}
}
container.register<Knife>('Knife', Knife)
class Soldier{
private weapon: TWeapon
constructor(weaponIdentifier: string){
this.weapon= container.get(weaponIdentifier)
}
public attack(victim: string){
this.weapon.start(victim)
}
}
container.register<Soldier>('SoldierG', Soldier, false, ['Gun'])
container.register<Soldier>('SoldierK', Soldier, false, ['Knife'])
const soldierG= container.get('SoldierG')
const soldierK= container.get('SoldierK')
soldierG.attack('dog') // gun is firing dog
soldierK.attack('pig') // knife is cutting pig
通过观察,很容易发现:每个模块定义完成之后需要注册进IoC容器;高层模块在实例化过程中需要先获取所有依赖。这两片代码的风格在调用时机上是非常鲜明和固定的,而且与模块的业务代码并非强关联。所以我们很容易想到利用AOP的思想将这两部分代码直接抽象成切面进行部署。好了,既然是切面,强大的装饰器语法该上场了。接下来我们分别需要实现一个类装饰器和属性装饰器:
function injectable(identifier: string, args: any[] = [], singleton : boolean = true): ClassDecorator{
return function(target: any){
container.register(identifier, target, singleton, args)
}
}
function inject(identifier: string): any{
return function (target: any, propertyKey: string){
container.registerMethod(identifier, target, propertyKey)
}
}
接下来,我们改造一下IoC容器,增加一个注册方法的函数registerMethod,并将注册信息保存在容器的methods属性中:
interface TModule<T>{
new(...args: any[]): T
}
interface TMethodsItem{
method: string,
identifier: string,
}
class Container{
private modules= new Map()
private methods= new WeakMap<Object, TMethodsItem[]>()
public register<T>(identifier: string, Module: TModule<T>, singleton: boolean = true, args: any[] = []){
this.modules.set(identifier, {
singleton,
args,
module: Module,
instance: null as T,
})
}
public registerMethod(identifier:string, target:any, propertyKey:string){
if(this.methods.has(target)){
this.methods.get(target).push({
method:propertyKey,
identifier
})
}else{
this.methods.set(target, [
{
method:propertyKey,
identifier
}
])
}
}
public get(identifier: string){
const moduleInfo= this.modules.get(identifier)
if(!moduleInfo.singleton || !moduleInfo.instance){
const methods= this.methods.get(moduleInfo.module.prototype)
moduleInfo.instance= new moduleInfo.module(...moduleInfo.args)
methods?.forEach((item: TMethodsItem) => moduleInfo.instance[item.method]= this.get(item.identifier))
}
return moduleInfo.instance
}
}
export const container= new Container()
好了,现在我们以装饰器注入的形式来实现对模块的注册。可以看到注册的过程不需要手动调用,而是通过一个类装饰器自动执行了:
interface TWeapon{ //定义需要实现的方法
start(victim: string): void, //start方法只接受一个string类型参数,无返回内容
}
@injectable('Gun')
class Gun implements TWeapon{
private fire(victim: string){
console.log('gun is firing ' + victim)
}
public start(victim: string){
this.fire(victim)
}
}
// container.register<Gun>('Gun', Gun)
@injectable('Knife')
class Knife implements TWeapon{
private cut(victim: string){
console.log('knife is cutting ' + victim)
}
public start(victim: string){
this.cut(victim)
}
}
// container.register<Knife>('Knife', Knife)
我们再增加一个Service模块,为Soldier模块增加更多的功能(方法):
@injectable('Service')
class Service{
public run(){
console.log('run')
}
public talk(){
console.log('talk')
}
}
这里我们可以重点看一下@inject装饰器的使用。可以看到Service模块的实例被自动注入进了高层模块的service属性,这样所有Soldier模块的实例都能调用到Service模块的方法了。
@injectable('SoldierG', [container.get('Gun')], false)
@injectable('SoldierK', [container.get('Knife')], false)
class Soldier{
@inject('Service') service: Service
private weapon: TWeapon
constructor(weaponInput: TWeapon){
this.weapon= weaponInput
}
public attack(victim: string){
this.weapon.start(victim)
}
}
// container.register<Soldier>('SoldierG', Soldier, false, ['Gun'])
// container.register<Soldier>('SoldierK', Soldier, false, ['Knife'])
const soldierG= container.get('SoldierG')
const soldierK= container.get('SoldierK')
soldierG.attack('dog') // gun is firing dog
soldierK.attack('pig') // knife is cutting pig
soldierG.service.run() // run
soldierK.service.talk() // talk
这种优美的对IoC思想的实现方式其实有一个专门的名词,叫做“依赖注入(DI)”。
什么是 DI?
DI 英文全称为 Dependency Injection,即依赖注入。依赖注入是控制反转最常见的一种应用方式,即通过控制反转,在对象创建的时候,自动注入一些依赖对象。
以上是我基于对DI的理解自己实现的代码,看起来会比较粗糙。下一文我将介绍一个强大而又轻量的控制反转容器InversifyJS,可用于编写稳定 TypeScript 和 JavaScript 应用。
前端小白,文中难免有谬误,请各位大佬轻喷!