使用TypeScript实现IoC设计模式

1,302 阅读2分钟

IoC的概念

IoC(Inversion of Control)称为 控制反转,是基于 依赖倒置 的设计模式。就是面向接口编程而不是面向实现编程,将所依赖的底层功能模块注入到高层模块中

比如定义一个人有哪些兴趣爱好这个模块,一般的会在模块里去具体实现,比如打篮球、游泳、旅行,但某天需求突然改了 这个人又get到一些新技能 弹琴、跳舞之类,那么只能去修改主模块,这让模块的扩展和维护变的很繁琐 也不能保证新功能会不会影响其他功能,很容易产生bug

文字临时图2222.png

用Ioc的思想改造下,将具体爱好变成一个个底层模块 然后根据需要 依赖注入到高层模块,这就是控制反转,高层定义接口而低层实现接口,当需要修改和新增低层功能模块时不会影响到上层 它们之间也不会互相影响,模块之间保持低耦合

具体实现

先用ES6实现下,首先创建一个IoC容器模块 CreateIoc

class CreateIoc {
    constructor() {
        this.container = new Map()
    }
    
    // 注入模块
    inject(...params) {
        this.handleInject(params, false)
    }
    
    // 注入模块(单例模式)
    injectSingleton(...params) {
        this.handleInject(params, true)
    }

    handleInject(params, singleton) {
        const [key, Fn] = params
        const callback = () => new Fn()
        this.container.set(key, { callback, singleton }) 
    }
    
    // 初始化使用模块
    init(key) {
        const obj = this.container.get(key)
        if (obj) {
          if (obj.singleton && !obj.instance) {
            obj.instance = obj.callback()
          }
        } else {
          console.error(`找不到"${key}"模块`)
        }
    }
}

将低层功能模块注入到高层模块,需要使用时再初始化调用其中的方法

class Basketball {
    constructor() {}
    play() { console.log('去打篮球') }
}

class Swim {
    constructor() {}
    play() { console.log('去游泳') }
}

const personIoc = new CreateIoc()

// 依赖注入
personIoc.inject('playBasketball', PlayBasketball)
personIoc.inject('swim', Swim)

// 只使用其中一个模块
const basketball = personIoc.init('playBasketball')
basketball.play()

使用 TypeScript 来改造下,增加一些要用到的静态类型

// 声明容器接口
interface IContainer {
  callback: () => {}
  singleton: boolean
  instance?: {}
}

// 声明可new实例的接口,并返回构造函数类型T
interface NewAble<T> {
  new (...args: any[]): T
}

// 参数类型(加入泛型用一个元组表示)
type TParams<T> = [key: string, Fn: NewAble<T>]

修改 class CreateIoc

class CreateIoc {
  private container = new Map<string, IContainer<any>>()
  
  // ...params 将参数展开为元组
  public inject<T>(...params: TParams<T>) {
    this.helpBind(params, false)
  }

  public injectSingleton<T>(...params: TParams<T>) {
    this.helpBind(params, true)
  }

  private handleInject<T>(params: TParams<T>, singleton: boolean) {
    const [key, Fn] = params
    const callback = <T>() => new Fn()
    // 这里使用 typeof Fn 将构造函数的类型提取出来
    const _instance: IContainer<typeof Fn> = { callback, singleton }
    this.container.set(key, _instance) 
  }
  
  public init<T>(key: string) {
    const obj = this.container.get(key)
    if (obj !== undefined) {
      if (obj.singleton && !obj.instance) {
        obj.instance = obj.callback()
      }
      // obj.instance存储了注入模块构造函数的实例
      return item.singleton ? <T>item.instance : <T>item?.callback()
    } else {
      console.error(`找不到"${key}"模块`)
    }
  }
}

使用时:

// 这里用接口实现的方式创建class
interface ISwim {
  play(str: string): void
}
class Swim implements ISwim {
  constructor() {}
  public goto(place: string): void {
    console.log(`去${place}游泳`)
  }
}

const personIoc = new CreateIoc()
// 注意一定要将接口作为泛型传入
personIoc.inject<ISwim>('swim', Swim)
const swim = personIoc.init<ISwim>('swim')
swim.goto('露天游泳池')

使用 ts-node 运行后成功显示期望的结果~~
npm i ts-node -g
ts-node xxx.ts

微信截图_20210720144620.png

最后总结下

IoC使用的依赖注入概念就是将低层模块作为参数的按需注入到高层模块中,高层模块通过加载器机制解耦对低层模块的依赖,而依赖于低层模块的抽象,低层模块对抽象进行具体实现,并通过注入器将依赖注入高层模块,这样很好保持了业务逻辑层的灵活