In the previous article “Decoupling ideas for the Front-end: From Interface to IoC Container”, we implemented an IoC container manually in accordance with IoC design ideas (code reference below), and used IoC container to realize the decoupling of dependencies between modules. The benefits of decoupling are obvious, but there is also the problem of cumbersome code (modules need to be manually registered with containers). And the need for high-level modules to manually obtain dependencies from the IoC container is inelegant and somewhat intrusive.

interface TModule<T>{
  new(... args:any[]): T
}


class Container{
  private modules= new Map(a)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=newmoduleInfo.module(... moduleInfo.args) }return moduleInfo.instance
  }
}

export const container= new Container()
Copy the code
interface TWeapon{  // Define the methods to be implemented
  start(victim: string) : void.// The start method takes a single string argument and returns nothing
}


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
Copy the code

From observation, it is easy to see: after each module definition is completed, it needs to be registered in the IoC container; A high-level module needs to obtain all dependencies during instantiation. The style of the two pieces of code is very distinct and fixed in terms of call timing and is not strongly related to the business code of the module. So it’s tempting to deploy these two pieces of code directly abstracted as slices using AOP thinking. Okay, now that it’s faceted, it’s time for the powerful decorator syntax. Next we need to implement a class decorator and a property decorator respectively:

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)
  }
}
Copy the code

Next, we will modify the IoC container to add a function registerMethod and store the registration information in the container’s methods property:

interface TModule<T>{
  new(... args:any[]): T
}

interface TMethodsItem{
  method: string.identifier: string,}class Container{
  private modules= new Map(a)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= newmoduleInfo.module(... moduleInfo.args) methods? .forEach((item: TMethodsItem) = > moduleInfo.instance[item.method]= this.get(item.identifier))
    }
    return moduleInfo.instance
  }
}

export const container= new Container()
Copy the code

Ok, now we register the module in the form of decorator injection. You can see that the registration process does not need to be called manually, but is performed automatically through a class decorator:

interface TWeapon{  // Define the methods to be implemented
  start(victim: string) :void.// The start method takes a single string argument and returns nothing
}

@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)
Copy the code

We add a Service module to add more functionality to the Soldier module (method) :

@injectable('Service')
class Service{
  public run(){
    console.log('run')}public talk(){
    console.log('talk')}}Copy the code

Here we can focus on the use of the @Inject decorator. You can see that instances of the Service module are automatically injected into the Service property of the higher level module, so that all instances of the Soldier module can call the Service module methods.

@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
Copy the code

This elegant implementation of the IoC idea actually has a name: Dependency injection (DI).

What is DI?

DI (Dependency Injection) Dependency injection (DI) is the most common application of inversion of control (INVERSION of control), which automatically injects dependent objects when objects are created.

The above code is my own implementation based on my understanding of DI, and it will look rough. In the next article I’ll introduce InversifyJS, a powerful and lightweight inversion of control container for writing stable TypeScript and JavaScript applications.

Front end small white, the article inevitably has fallacy, asks everybody big guy light spray!