前端也要懂的解耦思想:从IoC容器到DI实现

995 阅读4分钟

上一文《前端也要懂的解耦思想:从面向接口到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 应用。

前端小白,文中难免有谬误,请各位大佬轻喷!