上一文《前端也要懂的解耦思想:从面向对象到面向接口》我们已经通过依赖倒置原则(DIP),将模块间的依赖关系从具体实现细节转化成了抽象需求接口,即所谓的面向接口编程,从而实现了很大程度上的解耦。个人目前所遇到的前端项目,通过DIP原则+TS已经可以足够好的处理耦合关系。但随着工程化的日益加重,我们不得不保持忧患意识,思考进一步解耦的可能。
上文末我们提出了目前代码(参考如下)仍旧面临的一些问题隐患:比如我们将低层模块的实例化过程放在高层模块中,如果未来低层模块的构造方法发生了变化(如需要传参),则我们又不得不修改高层模块的代码。作为低层模块大概率要被多个高层模块依赖,这进一步加重了维护负担。那么接下来的解耦方向是非常明确的,就是处理模块实例化过程所导致的耦合关系。
//定义接口:
interface TWeaponClass{ //定义武器类型
new(): TWeapon
}
interface TWeapon{ //定义需要实现的方法
start(victim: string) : void, //start方法只接受一个string类型参数,无返回内容
}
class Gun implements TWeapon{ //约定武器必须实现start方法
private fire(victim: string){
console.log('gun is firing ' + victim)
}
public start(victim: string){
this.fire(victim)
}
}
class Knife implements TWeapon{
private cut(victim: string){
console.log('knife is cutting ' + victim)
}
public start(victim: string){
this.cut(victim)
}
}
class Soldier{
private weapon: TWeapon
constructor(InputWeapon: TWeaponClass){
this.weapon= new InputWeapon()
}
public attack(victim: string){ //attck方法只接受一个string类型参数
this.weapon.start(victim)
}
}
const soldierG= new Soldier(Gun)
const soldierS= new Soldier(Knife)
soldierG.attack('dog') // gun is firing dog
soldierS.attack('pig') // knife is cutting pig
既然是模块实例化过程导致的问题,那么我们或许会比较容易想到将各个模块统一管理。而所有模块的实例化过程则发生在这个管理中心。哪个模块需要依赖相关低层模块,就向这个管理中心去请求调用。这样一来,某个模块的构造方法有修改,则我们只需要在管理中心修改注册信息。这样的修改对于高层模块来说是透明或者无感的,因为管理中心已经帮他们处理好了一切,高层模块依旧原路请求调用即可。 如果你想到了这一层,其实你已经理解了后端领域大名鼎鼎的IoC设计思想!所谓的模块统一管理中心,就是IoC思想中的一个核心概念,IoC容器。
什么是IoC?
Inversion of Control,即控制反转。它不是一种技术,只是一种思想,一个重要的面向对象编程的法则。IoC思想能指导我们设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试。有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象。所以对象与对象之间是松散耦合,这样也方便测试,利于复用。
为什么叫做控制反转?
控制:指的是实例创建(实例化、管理)的权利。传统开发⽅式,我们直接在模块内部通过new进行创建实例,是程序主动去创建依赖对象。而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制实例对象的创建。
反转:控制权交给外部环境了(框架、IoC容器)。我们不⽤⾃⼰去new 实例了,⽽是由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()
可以看到IoC容器内容的代码相当简单,模块注册方法将模块信息以键值对的形式存放于容器内部一个Map中。get方法利用键获取模块实例(必要时先对模块实例化)。下面我们将模块代码重新设计一下,使其依赖模块的实例化权限全部交给IoC容器,实现控制反转。
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
现在我们将模块间的依赖关系做了彻底的解耦!虽然模块定义之后需要有一个注册的动作,但是带来的好处却是显而易见的。有任何的模块需要修改(如修改构造方法),则我们只需要在调用register方法时,修改输入参数即可(即模块注册信息),而依赖此模块的其他高层模块则不需要做任何的改动。这大大降低了项目维护负担,提升了整体稳定性。
但IoC的实现却也有一个缺点,就是代码略显繁琐不够优雅,而且在模块内部不得不直接引入IoC容器container,对逻辑造成了一定的侵入。
那是否有办法做优化呢?有的。下一文我们将介绍IoC的经典实现方案DI(依赖注入),使得IoC思想在解耦的同时,也在实现层面变得优雅简单。
前端小白,文中难免有谬误,请各位大佬轻喷!