共计 4195 个字符,预计需要花费 11 分钟才能阅读完成。
在大型项目的治理中,管制反转的思维是十分重要的。它能够帮忙咱们解耦代码,进步代码的可维护性。同时防止了不必要的反复实例化,升高内存透露的可能性。
而在 JS/TS 技术栈中,咱们通常会应用依赖注入框架来帮忙咱们治理服务。这其中最佳的抉择当然是 Angular 这种大而全的大型工程开发框架。而对于应用了其余 UI 框架的我的项目来说,咱们同样能够额定引入一个轻量化的依赖注入框架。而 InversifyJS 就是其中的佼佼者。咱们能够通过应用它,来见微知著地理解依赖注入的原理与设计哲学。
但最近在应用 Inversify 进行我的项目重构时,遇到了一个问题:家喻户晓依赖注入框架天生适宜治理单例服务。它的设计哲学是 Everything as Service
。然而在某些场景下,单例模式并不能解决所有问题,咱们同样须要进行多实例的治理。那么咱们该如何解决这个问题呢?
这并不是 Inversify 框架的问题,而其实是一个依赖注入框架下常见的设计纳闷,然而网上对此的解析材料却很少。
我看了很多应用了 InversifyJS 的我的项目,他们对此的形式就是间接在须要处实例化,不将其注册到容器中。这实际上是没有真正了解到依赖注入框架的内核。这样做的益处是简略,然而有很多弊病。因为咱们无奈在容器中对立治理这些实例,那么这些服务的生命周期将不受管制,在 dispose 时无奈在容器中对立销毁这些实例。与不引入依赖注入框架一样,这样同样会带来内存透露的可能性。
那么该如何正确地解决这种状况呢?
结构器注入
一个最简便的革新形式是,咱们将类的构造函数绑定到容器中。须要的时候从容器中获取类的结构器,再进行实例化。这样咱们就能够在容器中对立治理这些实例了。
// 将 InstanceClass 的构造函数绑定到容器中
container
.bind<interfaces.Newable<InstanceClass>>("Newable<InstanceClass>")
.toConstructor<InstanceClass>(InstanceClass);
// 获取结构器
public constructor(@inject("Newable<InstanceClass>") InstanceClass: Newable<InstanceClass>,
) {this.instance1 = new InstanceClass();
this.instance2 = new InstanceClass();}
实例会追随类的生命周期而存在,且该类能纳入容器中进行治理。然而这样做,实际上依然无奈在容器中对立治理这些实例的生命周期。如果咱们须要在 dispose 时销毁这些实例,那么咱们须要在类中手动实现 dispose 办法,并在 dispose 时手动销毁这些实例。
这样革新的益处是简略,然而很多时候并不是一个最优解,因为咱们心愿该实例自身能在注入框架的治理下,防止咱们去手动的管制与销毁。
工厂注入
依赖注入框架天生不太好治理多实例的服务,然而如果利用工厂模式的设计思维,将这些服务的实例化过程封装到工厂中,而这样的工厂类肯定是单例的。那么咱们就能够通过治理工厂类来治理这些多实例服务的生命周期了。
在须要多实例服务实例化时,咱们不间接 import 类进行实例化,而是通过 import 工厂类来获取实例。这样咱们就能够在工厂中管制多实例服务的生命周期了。
在 InversifyJS 中,提供了工厂注入的办法:
// 设置工厂函数
const instanceFactory = () => {return context.container.get<InstanceClass>("Instance");
};
// 工厂创立器,这里设置高阶函数的目标是将 context 传递给工厂函数,不便获取容器
const instanceFactoryCreator = (context: interfaces.Context) => {return instanceFactory;};
// 绑定工厂
container
.bind<interfaces.Factory<InstanceClass>>("Factory<InstanceClass>")
.toFactory<InstanceClass>(instanceFactoryCreator);
// 获取结构器
public constructor(@inject("Factory<InstanceClass>") private instanceFactory: () => InstanceClass,) {this.instance1 = this.instanceFactory();
this.instance2 = this.instanceFactory();}
这样的实现十分优雅,也是 Inversify 举荐的多实例治理形式。
当然,你也能够通过高阶函数的形式,生成不同的的工厂函数,以实现不同的实例化逻辑。
// 设置工厂函数
const instanceFactory = (name: string) => () => {if (name === "Instance") {return context.container.get<InstanceClass>("Instance");
}
return context.container.get<DefaultClass>("Default");
};
// 工厂创立器,这里设置高阶函数的目标是将 context 传递给工厂函数,不便获取容器
const instanceFactoryCreator = (context: interfaces.Context) => {return instanceFactory;};
// 绑定工厂
container
.bind<interfaces.Factory<InstanceClass>>("Factory<InstanceClass>")
.toFactory<InstanceClass>(instanceFactoryCreator);
在大多数状况下,它就是最规范的依赖注入框架下多实例治理形式了,也举荐能应用此形式的类尽量如此革新。
带参数实例化的工厂注入
当初重点来了,依赖注入框架完满解决了在类实例化时须要传入的依赖实例,防止了咱们须要在类的构造函数中获取或新建依赖实例。那么,对于那些依赖于传入内部上下文变量的类,咱们该如何解决呢?
这是咱们将已有的我的项目重构的过程中,常常会遇到的一种状况,这些类的构造函数执行过程依赖于内部上下文变量。
InversifyJS 的工厂注入在这中情景下的举荐实现形式比拟奇怪,是在获取实例后为实例进行属性注入。我大抵转写一下次要实现:
// 设置工厂函数
const instanceFactory = (payload: Record<string, any>) => {const instance = context.container.get<InstanceClass>("Instance");
instance.payload = payload;
return instance;
};
// 工厂创立器,这里设置高阶函数的目标是将 context 传递给工厂函数,不便获取容器
const instanceFactoryCreator = (context: interfaces.Context) => {return instanceFactory;};
// 绑定工厂
container
.bind<interfaces.Factory<InstanceClass>>("Factory<InstanceClass>")
.toFactory<InstanceClass>(instanceFactoryCreator);
在实例化后的运行时扭转实例的属性,从而使实例中对属性的依赖得以满足。但这样的实现形式,会使得咱们原有类的实现形式产生扭转,也会扭转类中属性的拜访形式,例如原来时 readonly 或是 private 的属性,咱们都无奈在运行时对其进行赋值。
当这个类继承于内部须要传入参数的类,或者是须要在首次实例化时依据传入的变量依赖执行局部操作时,这种实例化的形式是行不通的。
那么如果咱们的革新类具备以上个性,在不扭转原有实现形式的状况下,该当如何做呢?
咱们能够留神到,通过结构器注入的形式并不会将实例化时的行为交给容器,因而咱们能够在这里进行手动的实例化并传入参数。那这样的实例化形式同样能够与工厂模式相结合,实现带参数实例化的工厂注入。
// 设置工厂函数
const instanceFactory = (payload: Record<string, any>) => {
const InstanceClass = context.container.get<Newable<InstanceClass>>("Newable<InstanceClass>");
const instance = new InstanceClass(payload);
return instance;
};
留神,这里的 new InstanceClass
并不是援用原有类,而是援用了类的构造函数,而构造函数处于框架的管辖下,因而某种程度上该实例也是由框架进行了实例化得来的。因而,原有类甚至都不须要通过 @injectable
标注与注册。只需注册其结构器即可。
但始终,对于带参数实例化的工厂注入,它的实现形式并不优雅,也不合乎依赖注入的思维。因而,实质上来说,相似于 类继承
的形式并不是一个好的 code smell
,咱们举荐应用 对象组合
来代替 类继承
,从而躲避掉须要在构造函数中为 super() 传入变量的难堪场面。
结语
以上就是我在应用依赖注入框架重构我的项目时,对于多实例服务治理的一些思考与实际。它胜利地帮我实现了整个我的项目的重构,也让我对于依赖注入框架有了更深的了解。
但于此同时,我也在实践中发现了许多依赖注入框架的局限性。但这并不阐明依赖注入框架不够欠缺,而是阐明了依赖注入作为一种设计模式与思维,它有其匹配的设计哲学。例如在上述的例子中,真正依照框架的最佳实际来说,咱们该当只为服务注入行为形象,而不是某些具体的变量数据,这对代码可测性来说十分重要。
因而,我更举荐在应用依赖注入框架前,先学习依赖注入的设计思维,再去应用框架。而不是尝试魔改某个依赖注入框架来投合固有的编码格调。这不肯定对设计与性能有正向的收益。