通过示例深入浅出理解DI

624 阅读4分钟

前言

谈到DI,不得不先聊下IoC(控制反转),它只是一种思想,不是具体的技术,一个重要的OOP的法则,能指导如何设计出松耦合的程序。一般情况下我们都是在类内部主动创建依赖对象,从而导致类之间耦合高,难以测试。有了IoC,把创建和查找依赖对象的控制权嫁给容器,由容器进行诸如组合对象,所以对象之间的耦合度就松了,利于功能复用,保持系统灵活。

WHAT

那么,DI是什么,IoC跟DI(依赖注入)又有什么关系呢 ?这个概念是面相对象大师Martin Fowler2004年提出的,只是为了更明确的描述IoC,也可以理解为IoC的更合理的实现。所以两者本质是一样的。依赖注入的目的为了提升组件的复用频率,为系统提供灵活,扩展的平台,而不是为系统带来更多的功能。通过依赖注入,只需要简单的配置,无需任何代码就可以指定目标对象,完成自身的业务逻辑,不需要关心对象谁来创建和销毁。

HOW

在实际页面开发过程中,我们会创建很多service,诸如cache,user,logger等,一般我们会这样使用:

// 在Apage
import logger from '...'
import user from '...'
// 在Bpage
import logger from '...'
import user from '...'

很符合我们的编程习惯,但这种写法有个很大问题,耦合性太高。下面会通过一个示例重构的过程来分析和解决这个问题。

1. 页面A需要一个上报到logcenter的日志服务,我们可能这样编码:

class LoggerSvc {
	report(){console.log('logcenter report successfull~')}
}

class APage {
	logger = null
  constructor(){
  	this.logger = new LoggerSvc()
  }
  report(){
  	this.logger.report()
  }
}
const page  = new APage()
page.report()
  
// PRINT: logcenter report successfull~

2. 随着系统迭代,LoggerSvc要扩展新的上报方式,比如扩展Logan上报方式,那我们可以是用OO这样抽象:

interface Logger{
    report:() => void
}

class LogCenter implements Logger{
    report(){ console.log('logcenter report successfull~')}
}

class LoggerSvc {
  private logger:Logger
	report(){this.logger.report()}
  constructor(){
  	this.logger= new LogCenter()
  }
}

class APage {
	loggerSvc = null
  constructor(){
  	this.loggerSvc = new LoggerSvc()
  }
  report(){
  	this.loggerSvc.report()
  }
}

const page = new APage()
page.report()
  
// PRINT: logcenter report successfull~

3. 通过抽象,需要扩展一个实现Logger接口的Logan类并把LoggerSvc依赖的logger对象修改成Logan实例。

...
// 新增logan 
class Logan implements Logger{
    report(){ console.log('logan report successfull~')}
}
...
  

class LoggerSvc {
  private logger:Logger
	report(){this.logger.report()}
  constructor(){
    // 需要改动的地方
  	this.logger = new Logan()
  }
}
...

const page = new APage()
page.report()
//PRINT: logan report successfull~

大家会发现切换Logger的时候需要在LoggerSvc内部完成,造成LoggerSvc与Logan耦合性太强。有没有更好的办法呢?

结合上文提到的IoC思想,把对服务的依赖外置。那么只需要在使用的地方注入实例的方式完成切换。

...
class LoggerSvc {
  private logger:Logger
	report(){this.logger.report()}
  constructor(logger:Logger){
  	this.logger= logger
  }
}

class APage {
	loggerSvc = null
  constructor(loggerSvc:LoggerSvc){
  	this.loggerSvc = loggerSvc
  }
  ..
}

...

// 使用Ioc 只需改调用的地方即可完成切换
const logger =new Logan()  
// or
const logger = new LogCenter()
const loggerSvc = new LoggerSvc(logger)
const page = new APage(loggerSvc)
page.report()
  
  
//PRINT: logcenter report successfull~
// or 
//PRINT: logan report successfull~

但是我们仍然需要手动创建和管理依赖,能不能把对象的创建和依赖管理自动化呢?这时候DI就派上了用场。我这边用最熟悉的angular的方式模拟下实现过程。在原来的基础上只需要通过Injectable来注册依赖的服务,Injector来创建和管理依赖容器。是不是手动创建和管理的方式不见了。

import { Injectable, Injector } from '@angular/core'

abstract class Logger {
    report(): void { }
}

@Injectable()
class LogCenter extends Logger {
    report() { console.log('logcenter report') }
    constructor() { super() }
}


@Injectable()
class Logan extends Logger {
    report() { console.log('logan report') }
    constructor() { super() }
}


@Injectable()
class LoggerService {
    logger: Logger
    report() {
        this.logger.report()
    }
    constructor(logger: Logger) {
        this.logger = logger
    }
}



@Injectable()
class APage {
    constructor(private loggerService: LoggerService) { }
    report() {
        this.loggerService.report()
    }
}

// 创建容器
const inject = Injector.create({
    providers: [{
        provide: Logger, useClass: LogCenter, deps: []
    }, {
        provide: LoggerService, deps: [Logger]
    }, {
        provide: APage, deps: [LoggerService]
    }]
})

const page = inject.get(APage)

page.report()
  
//PRINT: logcenter report successfull~

只需要修改依赖配置表,可以快速完成切换。

...

const inject = Injector.create({
    providers: [{
      	// useClass换成Logan即可
        provide: Logger, useClass: Logan, deps: []
    }, {
        provide: LoggerService, deps: [Logger]
    }, {
        provide: APage, deps: [LoggerService]
    }]
})

...

//PRINT: logan report successfull~

Angular的DI毕竟跟框架太强。介绍一个通用的DI库InversifyJS,把上面的示例通过它来改造一下。

import 'reflect-metadata'
import { Container, injectable, inject, interfaces } from 'inversify'

// 类型声明
const TYEPS = {
    LogCenter: Symbol.for('LogCenter'),
    Logan: Symbol.for('Logan'),
    LoggerService: Symbol.for('LoggerService'),
    APage: Symbol.for('APage'),
    FactoryLogger: 'Factory<Logger>'
}


interface Logger {
    report: () => void
}

@injectable()
class LogCenter implements Logger {
    report() { console.log('logcenter report') }
}


@injectable()
class Logan implements Logger {
    report() { console.log('logan report') }
}


@injectable()
class LoggerService {
    logger: Logger
    report() {
        this.logger.report()
    }
  	// 标记注入FactoryLogger工厂,通过运行时判断
    constructor(@inject(TYEPS.FactoryLogger) loggerFactory: () => Logger) {
        this.logger = loggerFactory()
    }
}



@injectable()
class APage {
    constructor(@inject(TYEPS.LoggerService) private loggerService: LoggerService) { }
    report() {
        this.loggerService.report()
    }
}

// 创建容器
const container = new Container()
container.bind<Logger>(TYEPS.LogCenter).to(LogCenter)
container.bind<Logger>(TYEPS.Logan).to(Logan)
//为FactoryLogger 绑定Factory 函数
container.bind<interfaces.Factory<Logger>>(TYEPS.FactoryLogger).toFactory<Logger>((context: interfaces.Context) => {
    return () => {
        // 这个地方完成切换
      	// 1.使用LogCenter
        return context.container.get<Logger>(TYEPS.LogCenter)
        // 2.使用Logan
        return context.container.get<Logger>(TYEPS.Logan)
    };
});
container.bind<LoggerService>(TYEPS.LoggerService).to(LoggerService)
container.bind<APage>(TYEPS.APage).to(APage)

let page = container.get<APage>(TYEPS.APage)
page.report()

最后

通过示例优化重构的过程,帮助大家深入浅出的方式理解什么是DI。其本质上就是对象的控制权进行转移。DI已经是好多复杂的系统解耦的通用方式, 希望大家灵活运用。

参考

www.zhihu.com/question/25…

www.yuque.com/qinming-qvk…