Async-Injection
用于 TypeScript 的强大的轻量级依赖注入库。
About
Async-Injection 是一个小型 IoC 容器,支持同步和异步依赖注入,以及隔离和/或分层范围。
Installation
您可以使用 npm 获取最新版本:
$ npm install async-injection --save
Basic Usage (synchronous)
这里我们“获取”一个新的事务处理对象,它本身依赖于共享服务:
@Injectable()
class SharedService {
constructor(@Inject('LogLevel') @Optional('warn') private logLevel: string) { }
}
@Injectable()
class TransactionHandler {
constructor(svc: SharedService) { }
}
// Create a simple container (we will bind providers into it).
const container = new Container();
// A single instance will be created and shared by everyone.
container.bindClass(SharedService).asSingleton();
// A new instance will be created each time one is requested.
container.bindClass(TransactionHandler);
// If we omit this line, the logLevel of SharedService will be initialized to 'warn'
container.bindConstant('LogLevel', 'info');
// In our request processing code (which would be an anti-pattern)...
// Instantiate a new transaction handler (it will be injected with the shared service).
const tx = container.get(TransactionHandler);
注意:
本自述文件中的示例旨在快速传达概念和用法。
您的真实世界项目当然应该遵循最佳实践,例如关注点分离,复合根,并且应该避免像服务定位器。
Scopes
可以使用多个容器和/或容器层次结构来创建范围。
IoC Modules
为什么要重新发明轮子? 打字稿很棒!
实现您想要的“模块”并导入它:
my-http-ioc-module.ts
import {myContainer} from './app';
import {Logger, HttpClient} from './services';
import {HttpClientGotWrapper} from './impl';
myContainer.bind(Logger).asSingleton();
myContainer.bind(HttpClient, HttpClientGotWrapper);
Asynchronous Support
为简单起见,建议您尽可能对任何类使用传统的同步注入。
但是当这只是很多工作时,您可以“混合”同步和异步注入。
为了支持“混合”,我们在 Container
上引入了三种新方法,下面将对此进行说明。
Asynchronous Usage
也许在上面的示例中,我们的 SharedService
在建立数据库连接之前是无用的。
当然,这样一个简单的场景可以很容易地在用户端代码中处理,但是随着应用程序复杂性的增加,这会变得更加繁琐且难以维护。
让我们修改示例如下:
@Injectable()
class SharedService {
constructor() { }
connect(): Promise<void> { ... }
}
const container = new Container();
// Bind a factory function that awaits until it can fully create a SharedService.
container.bindAsyncFactory(SharedService, async () => {
let svc = new SharedService();
return await svc.connect();
}).asSingleton();
// A new transient instance will be created each time one is requested.
container.bindClass(TransactionHandler);
// Wait for all bound asynchronous factory functions to complete.
// This step is optional. You could omit and use Container.resolve instead (see alternative below).
await container.resolveSingletons(true);
// We are now connected to the database
// In our request processing code...
const tx = container.get(TransactionHandler);
作为替代方案,我们可以删除对 Container.resolveSingletons
的调用,在我们的请求处理代码中,只需调用 Container.resolve 。
const tx = await container.resolve(TransactionHandler);
Important - Container.resolve vs Container.get
混合同步和异步注入会增加应用程序的复杂性。
成功混合的关键是将您请求的对象视为一个对象,而不是一个对象,而您的对象位于顶部。
请记住,您可能需要每次都创建transient 对象,以及依赖关系树中现有的singleton 对象。
如果您提前知道您所依赖的每个对象都是立即(同步)可用的,或者如果它们是异步的单例并且已经被解析(通过 Container.resolveSingletons
,或者之前调用过Container.resolve
),那么不用等待,直接Container.get
就可以了。
否则,您需要使用 await Container.resolve
等待树的完整解析。
@PostConstruct Support
并不总是可以在类构造函数中完全初始化您的对象。
这个(尽管是人为的)演示表明,当 Person
子类尝试调用重写的 state
方法时,Employee
类尚未初始化。
class Person {
public constructor() { this.describe(); }
protected state() { return "relaxing"; }
public describe() { console.log("Hi I'm '" + this.state() + "'"); }
}
class Employee extends Person {
constructor(private manager: boolean) { super(); }
protected state() { return this.manager ? "busy" : "producing"; }
}
// This will print:
// "Hi I'm 'producing", even though the author probably expected
// "Hi I'm busy", because they passed true for the 'manager' parameter.
new Employee(true);
我们可以重构代码来解决这个问题吗? 当然。 您可能需要提交几个 PR、重写没有单元测试的遗留代码、垃圾封装、跳过几个晚上的睡眠等等。但为什么呢?
PostConstruct 注释确保您的初始化方法在您的对象的完整构建版本上工作。
更好的是,由于构造函数不能是异步的,因此 PostConstruct 为您提供了一种在对象投入使用之前异步准备对象的简便方法。
@PostConstruct Usage
后构造方法可以是同步的也可以是异步的。
class A {
public constructor() { }
// Called before the object is placed into the container (or is returned from get/resolve)
@PostConstruct()
public init(): void { ... }
}
class D {
public constructor() { }
// Will not be placed into the container (or returned) until the Promise has been resolved.
@PostConstruct()
public init(): Promise<void> { ... }
}
@PostConstruct Guidelines:
- Ensure your post construction method signature properly declares it's return type.
WARNING! An unspecified return type signature where the type is implied by
return new Promise(...)
is not sufficient! You must explicitly declare the return type.
Container.get
will throw an exception if you try to retrieve a class with @PostConstruct
on a method that returns a Promise
, but which does not declare it's return type to be Promise
.
- The library will not invoke @PostConstruct on an object returned from a factory. It is the factory's responsibility to construct and initialize before returning.
- You will likely want a
Container.resolveSingletons(true)
call between your last Container.bindXXX()
call and any Container.get
call.
API Overview
异步注入尝试遵循在大多数其他 DI 实现中发现的常见 API 模式。 具体语法请参考上面的示例或下面的链接元素。
- The
Container class implements a
Binder interface, which allows you to bind a
Constant,
Factory,
AsyncFactory, or
Class value to an
InjectableId (aka key) within a
Container.
- The
Container also implements an
Injector interface which allows you to synchronously
get or asynchronously
resolve anything that has been bound.
- When binding a
Factory,
AsyncFactory or
Class to an
InjectableId, you can chain the result of the call to specify the binding as a
Singleton, and/or configure an
Error Handler.
- Containers may be nested by passing a parent Container to the
constructor of a child Container.
- To bind a
Class into the
Container, you must add the
@Injectable decorator (aka annotation) to your class (see examples above).
- You may optionally add a
@PostConstruct decorator to a method of your class to perform synchronous or asynchronous initialization of a new instance.
- By default, Async-Inject will examine the parameters of a class constructor and do it's best to match those to bound
InjectableIds.
- You may use the
@Inject decorator to explicitly declare which
InjectableId should be used (generally required for a
Constant binding as in the logLevel example above).
- The
@Optional decorator allows you to specify a default value for a class constructor parameter in the event that no matching
InjectableId can be found.
- The Container's
resolveSingletons method may be used to wait for any bound asynchronous Singletons to finish initialization before continuing execution of your application.
Acknowledgements
感谢 InversifyJS 的所有贡献者。 它是一个强大、干净、灵活、鼓舞人心的设计。
感谢 NestJS 为我们提供异步提供程序的每个人。
感谢 Darcy Rayner 如此简单明了地描述了 DI 实现。
感谢 Carlos Delgado 提出 "QuerablePromise" 允许我们将异步 DI 与同步 DI 的简单性结合起来。
MIT License
版权所有 (c) 2020 Frank Stock
特此免费向任何获得副本的人授予许可
本软件和相关文档文件(“软件”),处理
在软件中不受限制,包括但不限于权利
使用、复制、修改、合并、发布、分发、再许可和/或出售
该软件的副本,并允许该软件是
提供这样做,但须满足以下条件:
上述版权声明和本许可声明应包含在所有
软件的副本或重要部分。
本软件“按原样”提供,不提供任何形式的明示或保证
暗示的,包括但不限于适销性保证,
适用于特定目的和非侵权。 在任何情况下都不得
作者或版权持有人对任何索赔、损害或其他
责任,无论是在合同、侵权或其他方面的行为中,由以下原因引起,
出于或与软件或使用或其他交易有关
软件。
Async-Injection
A robust lightweight dependency injection library for TypeScript.
About
Async-Injection is a small IoC container with support for both synchronous and asynchronous dependency injection, as well as isolated and/or hierarchical scopes.
Installation
You can get the latest release using npm:
$ npm install async-injection --save
Basic Usage (synchronous)
Here we 'get' a new transaction handling object, that itself, relies on a shared service:
@Injectable()
class SharedService {
constructor(@Inject('LogLevel') @Optional('warn') private logLevel: string) { }
}
@Injectable()
class TransactionHandler {
constructor(svc: SharedService) { }
}
// Create a simple container (we will bind providers into it).
const container = new Container();
// A single instance will be created and shared by everyone.
container.bindClass(SharedService).asSingleton();
// A new instance will be created each time one is requested.
container.bindClass(TransactionHandler);
// If we omit this line, the logLevel of SharedService will be initialized to 'warn'
container.bindConstant('LogLevel', 'info');
// In our request processing code (which would be an anti-pattern)...
// Instantiate a new transaction handler (it will be injected with the shared service).
const tx = container.get(TransactionHandler);
NOTE:
The examples in this ReadMe are contrived to quickly communicate concepts and usage.
Your real world project should of course follow best practices like separation of concerns, having a composition root, and should avoid anti-patterns like service locator.
Scopes
Scopes can be created using multiple Containers, and/or a hierarchy of Containers.
IoC Modules
Why reinvent the wheel? TypeScript is great!
Implement the "module" you want and just import it:
my-http-ioc-module.ts
import {myContainer} from './app';
import {Logger, HttpClient} from './services';
import {HttpClientGotWrapper} from './impl';
myContainer.bind(Logger).asSingleton();
myContainer.bind(HttpClient, HttpClientGotWrapper);
Asynchronous Support
For simplicity, it is recommended that you use traditional synchronous injection for any class where that is possible.
But when that's just to much work, you can "blend" synchronous and asynchronous injection.
To support "blending", we introduce three new methods on the Container
which will be explained below.
Asynchronous Usage
Perhaps in the example above, our SharedService
is useless until it has established a database connection.
Of course such a simple scenario could easily be handled in user-land code, but as application complexity grows, this becomes more tedious and difficult to maintain.
Let's modify the example as follows:
@Injectable()
class SharedService {
constructor() { }
connect(): Promise<void> { ... }
}
const container = new Container();
// Bind a factory function that awaits until it can fully create a SharedService.
container.bindAsyncFactory(SharedService, async () => {
let svc = new SharedService();
return await svc.connect();
}).asSingleton();
// A new transient instance will be created each time one is requested.
container.bindClass(TransactionHandler);
// Wait for all bound asynchronous factory functions to complete.
// This step is optional. You could omit and use Container.resolve instead (see alternative below).
await container.resolveSingletons(true);
// We are now connected to the database
// In our request processing code...
const tx = container.get(TransactionHandler);
As an alternative, we could remove the call to Container.resolveSingletons
, and in our request processing code, simply call Container.resolve
.
const tx = await container.resolve(TransactionHandler);
Important - Container.resolve vs Container.get
Blending synchronous and asynchronous injection adds complexity to your application.
The key to successful blending is to think of the object you are requesting, not as an object, but as a tree of objects with your object at the top.
Keep in mind that you may have transient objects which need to be created each time, as well as existing singleton objects in your dependency tree.
If you know ahead of time that every object which you depend on is immediately (synchronously) available, or if they are asynchronous singletons which have already been resolved (via Container.resolveSingletons
, or a previous call to Container.resolve
), then no need to wait, you can just Container.get
the tree.
Otherwise you need to await the full resolution of the tree with await Container.resolve
.
@PostConstruct Support
It is not always possible to fully initialize your object in the class constructor.
This (albeit contrived) demo shows that the Employee
class is not yet initialized when the Person
subclass tries to call the overridden state
method.
class Person {
public constructor() { this.describe(); }
protected state() { return "relaxing"; }
public describe() { console.log("Hi I'm '" + this.state() + "'"); }
}
class Employee extends Person {
constructor(private manager: boolean) { super(); }
protected state() { return this.manager ? "busy" : "producing"; }
}
// This will print:
// "Hi I'm 'producing", even though the author probably expected
// "Hi I'm busy", because they passed true for the 'manager' parameter.
new Employee(true);
Can we refactor code to work around this? Sure. You may have to submit a couple of PR's, re-write legacy code that has no unit tests, trash encapsulation, skip a few nights sleep, etc. But why?
A PostConstruct annotation ensure's your initialization method is working on a fully constructed version of your object.
Even better, since constructors cannot be asynchronous, PostConstruct gives you an easy way to asynchronously prepare an object before it's put into service.
@PostConstruct Usage
Post construction methods can be either synchronous or asynchronous.
class A {
public constructor() { }
// Called before the object is placed into the container (or is returned from get/resolve)
@PostConstruct()
public init(): void { ... }
}
class D {
public constructor() { }
// Will not be placed into the container (or returned) until the Promise has been resolved.
@PostConstruct()
public init(): Promise<void> { ... }
}
@PostConstruct Guidelines:
- Ensure your post construction method signature properly declares it's return type.
WARNING! An unspecified return type signature where the type is implied by
return new Promise(...)
is not sufficient! You must explicitly declare the return type.
Container.get
will throw an exception if you try to retrieve a class with @PostConstruct
on a method that returns a Promise
, but which does not declare it's return type to be Promise
.
- The library will not invoke @PostConstruct on an object returned from a factory. It is the factory's responsibility to construct and initialize before returning.
- You will likely want a
Container.resolveSingletons(true)
call between your last Container.bindXXX()
call and any Container.get
call.
API Overview
Async-Injection tries to follow the common API patterns found in most other DI implementations. Please refer to the examples above or the linked elements below for specific syntax.
- The
Container class implements a
Binder interface, which allows you to bind a
Constant,
Factory,
AsyncFactory, or
Class value to an
InjectableId (aka key) within a
Container.
- The
Container also implements an
Injector interface which allows you to synchronously
get or asynchronously
resolve anything that has been bound.
- When binding a
Factory,
AsyncFactory or
Class to an
InjectableId, you can chain the result of the call to specify the binding as a
Singleton, and/or configure an
Error Handler.
- Containers may be nested by passing a parent Container to the
constructor of a child Container.
- To bind a
Class into the
Container, you must add the
@Injectable decorator (aka annotation) to your class (see examples above).
- You may optionally add a
@PostConstruct decorator to a method of your class to perform synchronous or asynchronous initialization of a new instance.
- By default, Async-Inject will examine the parameters of a class constructor and do it's best to match those to bound
InjectableIds.
- You may use the
@Inject decorator to explicitly declare which
InjectableId should be used (generally required for a
Constant binding as in the logLevel example above).
- The
@Optional decorator allows you to specify a default value for a class constructor parameter in the event that no matching
InjectableId can be found.
- The Container's
resolveSingletons method may be used to wait for any bound asynchronous Singletons to finish initialization before continuing execution of your application.
Acknowledgements
Thanks to all the contributors at InversifyJS. It is a powerful, clean, flexible, inspiring design.
Thanks to everyone at NestJS for giving us Asynchronous providers.
Thanks to Darcy Rayner for describing a DI implementation so simply and clearly.
Thanks to Carlos Delgado for the idea of a "QuerablePromise" which allowed us to blend asynchronous DI with the simplicity of synchronous DI.
MIT License
Copyright (c) 2020 Frank Stock
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.