关于依赖注入(typescript)

10,551 阅读4分钟

一、概念:依赖注入(DI)、控制反转(IOC)、IOC容器

依赖注入(DI)和控制反转(IOC)基本是一个意思,因为说起来谁都离不开谁。 简单来说,类A依赖类B,但A不控制B的创建和销毁,仅使用B,那么B的控制权则交给A之外处理,这叫控制反转(IOC)。 由于A依赖于B,因此在A中必然要使用B的instance,我们可以通过A的构造函数将B的实例注入,比如:

class B { }
class A {
  constructor(b: B) { 
      console.log(b);
  }
}
const b = new B();
// 将B的实例注入到a中
const a = new A(b);

这个过程叫依赖注入(DI)。 那么什么是IOC Container(容器)? 在刚刚的例子中,将B的实例注入到A的构造函数中的这个过程是我们手动操作的,比较麻烦,特别是当类的关系变多变复杂时,这种方式显得很难维护。 因此IOC容器就是为了解决这样的问题,IOC容器负责管理对象的生命周期、依赖关系等,实现对象的依赖查找以及依赖注入。 比如Java的Spring以及前端@Angular框架的依赖注入器(DI)就是属于IOC容器。

接下来我将通过代码的形式对比使用依赖注入相比非依赖注入的好处体现在哪。

二、非依赖注入代码

我们先来看一段传统的实现代码(非DI) car.ts

// 引擎 
export class Engine {
  public cylinders = '引擎发动机1';
}
// 轮胎
export class Tires {
  public make = '品牌';
}
export class Car {
  public engine: Engine;
  public tires: Tires;
  public description = 'No DI'; 
  constructor() {
    this.engine = new Engine();
    this.tires = new Tires();
  }
  // Method using the engine and tires
  drive() {
    return `${this.description} car with ` +
      `${this.engine.cylinders} cylinders and ${this.tires.make} tires.`;
  }
}

在以上代码中,Car类没有通过第三方容器而是亲自创建了一个引擎(engine)和一些轮胎(tires),这样的代码耦合度比较高,这样会存在以下问题:

问题1:如果有一天对引擎进行升级,代码如下:

// 引擎  
export class Engine {
  public cylinders = '';
  constructor(_cylinders:string) {
    this.cylinders = _cylinders;
  }
}

在创建引擎的时候需要传入一个参数,那么这时候就需要修改Car类里的new Engine(parameter),这样就导致Car类被破坏了,这里请思考一个问题:要怎么做才能使引擎升级的时候不需要修改Car类呢?(答案:DI)

问题2:如果想在Car上使用不同品牌的轮胎,代码如下:

// 轮胎
export class Tires {
  public make = '品牌';
}
export class Tires1 extends Tires {
  public make = '品牌1';
}
export class Tires2 extends Tires {
  public make = '品牌2';
}
export class Car {
   //。。。。。。其他代码省略。。。。。。。
  public tires: Tires;
  constructor() {
    this.tires = new Tires1();
  }
}

此时又得重新修改Car的代码,这里请思考一个问题:要怎么做才能使Car更换其他不同品牌的轮胎的时候不需要修改Car类呢?(答案:DI)

问题3:如何实现数据共享,比如说车联网,建立了一个Service数据中心,不同的Car通过Service实现数据通信以及数据共享,如果是通过在Car里new Service的方式,是无法实现数据共享和通信的,因为不同Car里的Service不是同一个实例。

这里请思考一个问题:如何实现不同Car的数据通信和共享呢?

问题4:测试比较难,根本无法测试。 在示例代码中,Car类依赖于Engine类和Tires类,而Engine和Tires又可能各自依赖于其他的类,而其他的类又可能有各自更多的依赖,在这样层层的依赖关系中,由于不能控制Car背后的隐藏依赖,要进行测试是比较难的,或者应该说,这样的代码是根本无法进行测试的。 比如说想同时测试不同品牌的轮子的car的性能,因为car里头的new已经写死了,因此无法做到。 比如说想同时测试不同参数的引擎的car的性能,因为car里头的new已经写死了,因此无法做到。 除非是每次只测试一种情况,下面拿测试不同品牌的轮子来举例: 先测试品牌1的轮子: car.ts

export class Tires {
  public make = '品牌';
}
export class Tires1 extends Tires {
  public make = '品牌1';
}
export class Tires2 extends Tires {
  public make = '品牌2';
}
export class Car {
  public tires: Tires;
  public description = 'No DI'; 
  constructor() {
    // new 一个品牌1的轮子
    this.tires = new Tires1();
  }
  // Method using the engine and tires
  drive() {
    return `${this.description} car with ` + ` ${this.tires.make} tires.`;
  }
}

测试程序car.spec.ts

import { Car } from './car.ts';

describe('Car类单元测试', function () {
  it('测试品牌1轮子的Car的驾驶性能', function () {
    const car = new Car();
    car.drive().should.equal('No DI car with 品牌1 tires.');
  })
})

以上代码对轮子品牌1进行测试,输出轮子品牌1的car的驾驶性能。 接着对轮子品牌2进行测试:修改Car类,将this.tires = new Tires1();修改为 this.tires = new Tires2();此时输出轮子品牌2的car的驾驶性能。

这样的测试效率是很低的,因为每次只能手动的测试一种情况,如果再加上引擎的测试,那多种混合情况就更多了,根本就不能做到自动测试,所谓的自动测试,是一次性将所有的情况都写到一个单元测试里,一次运行,所有情况都会被测试到,当测试通过了,那么就说明代码达到了预期。

针对以上问题,我们来看看使用DI的好处。

三、使用依赖注入(DI)

接下来将演示使用DI来解决以上的4个问题。 先看使用DI实现的car.ts代码: car.ts

export class Engine {
  public cylinders = '引擎发动机1';
}
export class Tires {
  public make = '品牌';
}
export class Tires1 extends Tires {
  public make = '品牌1';
}
export class Tires2 extends Tires {
  public make = '品牌2';
}
export class Car {
  public description = 'DI'; 
  // 通过构造函数注入Engine和Tires
  constructor(public engine: Engine, public tires: Tires) {}  
  // Method using the engine and tires
  drive() {
    return `${this.description} car with ` +
      `${this.engine.cylinders} cylinders and ${this.tires.make} tires.`;
  }
}

在以上代码中,通过往构造函数中传入engine和tires来创建Car,Car类不再亲自创建engine和tires,而是消费它们,此时最大的好处就是engine和tires与Car解除了强耦的关系。在new Car的时候,可以传入任何类型的Engine和Tires,即 let car = new Car(new Engine(),new Tires());

解决问题1:如果有一天对引擎进行升级,代码如下:

export class Engine {
  public cylinders = '';
  constructor(_cylinders:string) {
    this.cylinders = _cylinders;
  }
}

在创建引擎的时候需要传入一个参数,这时候不需要修改Car类,只需要修改主程序即可:

主程序代码:

main(){
    const car = new Car(new Engine('引擎启动机2'), new Tires1());
    car.drive();
}

解决问题2:如果想在Car上使用不同品牌的轮胎,代码如下:

export class Tires {
  public make = '品牌';
}
export class Tire1 extends Tires {
  public make = '品牌1';
}
export class Tire2 extends Tires {
  public make = '品牌2';
}
export class Car {
   //。。。。。。其他代码省略。。。。。。。
  constructor(public engine: Engine, public tires: Tires) {}  
}

此时不需要修改Car类,只需要修改主程序即可: 主程序代码:

main(){
  // 使用品牌2的轮胎
  const car = new Car(new Engine('引擎启动机2'), new Tires2());
  car.drive();
}

解决问题3:如何实现数据共享,比如说车联网,建立一个Service数据中心(就像angular的Service层,可以给多个component共享),不同的Car通过Service实现数据通信以及数据共享。 代码如下: Service.ts

export class Service {
  public data = '';
  // 向Service存数据
  setData(_data: string) {
    this.data = _data;
  }
  // 从Service中取数据
  getData() {
    return this.data;
  }
}

car.ts

export class Car {
  constructor(public service: Service) { }
  // 向Service存数据
  setDataToService(_data: string) {
    this.service.setData(_data);
  }
  // 从Service中取数据
  getDataFromService() {
    return this.service.getData();
  }
}

此时主程序如下: 主程序代码:

main(){
  // 创建一个共享服务中心Service
  const shareService = new Service();
  const car1 = new Car(shareService);
  const car2 = new Car(shareService);
  // car1向服务中心存数据
  car1.setDataToService('this data is from car1.');
  // car2从服务中心取数据
  car2.getDataFromService();
}

解决问题4:测试用例 在示例代码中,Car类依赖于Engine类和Tires类,而Engine和Tires又可能各自依赖于其他的类,而其他的类又可能有各自的依赖,在这样层层的依赖关系中,使用DI的代码测试是比较简单的。 测试程序如下: 测试程序 car.spec.ts

import { Car,Engine,Tires1, Tires2} from './car.ts';
// 测试程序入口
describe('Car类单元测试', function () {
  const engine1 = new Engine('引擎发动机1');
  const engine2 = new Engine('引擎发动机2');
  const tires1 = new Tires1();
  const tires2 = new Tires2();

  it('测试引擎1 轮胎品牌1', function () {
    const car = new Car(engine1, tires1);
    car.drive().should.equal('DI car with 引擎发动机1 cylinders and 品牌1 tires.');
  });
  it('测试引擎1 轮胎品牌2', function () {
    const car = new Car(engine1, tires2);
    car.drive().should.equal('DI car with 引擎发动机1 cylinders and 品牌2 tires.');
  });
  it('测试引擎2 轮胎品牌1', function () {
    const car = new Car(engine2, tires1);
    car.drive().should.equal('DI car with 引擎发动机2 cylinders and 品牌1 tires.');
  });
  it('测试引擎2 轮胎品牌2', function () {
    const car = new Car(engine2, tires2);
    car.drive().should.equal('DI car with 引擎发动机2 cylinders and 品牌2 tires.');
  });
    
})

此时觉得很棒有木有,自动测试的思想就是这样的,将所有的情况的代码都配置好,一次运行,所有的情况都可以测试到。

至此,如果看懂以上的话,DI的思想以及为什么要用DI就应该可以理解了。