Last active
April 28, 2023 10:36
-
-
Save Romakita/fb86225dffcfa2f28bbcbce9490e3668 to your computer and use it in GitHub Desktop.
How to build and IOC. This exemple explore different way to resolve dependencies
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {Provider} from "./interfaces"; | |
import {parseSignature} from "./parseSignature"; | |
export function getDeps(provider: Provider) { | |
// resolving deps strategies | |
// 1. by provider deps | |
if (provider.deps?.length) { | |
return provider.deps; | |
} | |
// 2. by constructor | |
if (provider.useClass) { | |
if (Reflect) { | |
// 2a. by typescript metadata (remove this if you don't use typescript emitDecoratorMetadata) | |
return Reflect.getMetadata("design:paramtypes", provider.useClass) || []; | |
} | |
// 2b. by parsing constructor signature | |
return parseSignature(provider.useClass.toString()); | |
} | |
// 3. try to parse factory signature | |
return parseSignature(provider.useFactory || provider.useAsyncFactory); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Typescript legacy decorator to inject provider | |
*/ | |
export function Inject(token?: TokenProvider | () => TokenProvider) { | |
return (target: any, property: string, index: number) => { | |
const deps = Reflect.getMetadata("design:paramtypes", target.prototype) || []; | |
deps[index] = token; | |
Reflect.defineMetadata("design:paramtypes", deps, target.prototype, propertyKey); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {registerProvider} from "./registerProvider"; | |
import {Provider} from "./interfaces"; | |
// decorator legacy | |
export function Injectable(opts: Partial<Omit<Provider, "useClass" | "useFactory" | "useAsyncFactory">) { | |
return (target: any) => { | |
registerProvider({token: target, ...opts}); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {Providern, TokenProvider, LocalsContainer, Type} from "./interfaces"; | |
import {getDeps} from "./getDeps"; | |
export class InjectorService { | |
#cache = new Map<TokenProvider, any>(); | |
constructor() { | |
// make injector service to be injectable | |
this.set(InjectorService, this); | |
} | |
private set(token: TokenProvider, instance: any) { | |
const name = nameOf(token); | |
this.#cache.set(InjectorService, this); | |
this.#cache.set(name, this); // required is you use angular.js convention | |
} | |
has(token: TokenProvider) { | |
return this.has(token) || this.has(nameOf(token)); | |
} | |
get<T>(token: TokenProvider): T { | |
return (this.#cache.get(token) || this.#cache.get(nameOf(token))) as T; | |
} | |
async loadAsync(locals: LocalsContainer) { | |
for (const [, provider] of providersRegistry) { | |
if (!this.has(provider.token) && provider.useAsyncFactory) { | |
await this.invoke(provider.token, locals); | |
} | |
} | |
} | |
loadSync(locals: LocalsContainer) { | |
for (const [, provider] of providersRegistry) { | |
if (!this.has(provider.token)) { | |
this.invoke(provider.token, locals); | |
} | |
} | |
} | |
async load(locals: Map<TokenProvider, any> = new Map()) { | |
// build async and sync provider | |
await this.loadAsync(locals); | |
// load sync provider | |
this.loadSync(locals); | |
await this.emit("$onInit()"); | |
} | |
invoke<T = any>(token: TokenProvider, locals: LocalsContainer = new Map(), opts: { rebuild?: boolean } = {}): T { | |
const provider = providersRegistry.get(token); | |
if (!provider) { | |
throw new Error(`Provider not found for token ${String(token)}`); | |
} | |
// 1. resolve from locals first | |
const local = locals.get(token); | |
if (local) { | |
return local; | |
} | |
if (!rebuild){ | |
// 2. return already resolved instance | |
const instance = this.#cache.get(provider.token); | |
if (instance) { | |
return instance; | |
} | |
} | |
const invokable = this.resolve(provider, locals, opts); | |
return invokable(); | |
} | |
async clear() { | |
await this.emit("$onDestroy()"); | |
this.#cache.clear(); | |
} | |
emit(event: string, ...args: any[]) { | |
const promises = [...providersRegistry.values()].map((provider) => { | |
const instance = this.get<any>(provider.token); | |
if (instance) { | |
if (event in instance) { | |
return instance[event](...args); | |
} | |
if (event in provider.hooks) { | |
return provider.hooks[event](instance, ...args); | |
} | |
} | |
}); | |
return Promise.all(promises); | |
} | |
private resolve(provider: Provider, locals: LocalsContainer, opts?: { rebuild?: boolean }) { | |
const tokenDependencies = getDeps(provider); | |
const args = tokenDependencies.map((token) => this.invoke(token, locals, opts)); | |
if (provider.useClass) { | |
return () => { | |
const instance = new provider.useClass(...args); | |
locals.set(provider.token, instance); | |
this.set(provider.token, instance); | |
return instance; | |
}; | |
} | |
if (provider.useFactory) { | |
return async () => { | |
const resolved = await Promise.all(args); | |
const instance = provider.useFactory(...resolved); | |
this.set(provider.token, instance); | |
locals.set(provider.token, instance); | |
return instance; | |
}; | |
} | |
if (provider.useAsyncFactory) { | |
return () => { | |
const instance = provider.useAsyncFactory(...args); | |
this.set(provider.token, instance); | |
locals.set(provider.token, instance); | |
return instance; | |
}; | |
} | |
return undefined; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* An example of a `Type` is `MyCustomComponent` filters, which in JavaScript is be represented by | |
* the `MyCustomComponent` constructor function. | |
*/ | |
export interface Type<T = any> extends Function { | |
new(...args: any[]): T; | |
} | |
export const Type = Function; | |
export type TokenProvider = string | symbol | Type; | |
export interface Provider<InstanceType = any> { | |
token: TokenProvider, | |
deps?: TokenProvider[], | |
useClass?: Type<InstanceType>, | |
hooks?: Record<string, (instance: InstanceType, ...args: any[]) => any | Promise<any>> | |
useAsyncFactory?(...args: any[]): Promise<any>, | |
useFactory?(...args: any[]): any, | |
} | |
export type LocalsContainer = Map<TokenProvider, any>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export function parseSignature(token: any): string[] { | |
const content = token.toString(); | |
let signature; | |
const extract = str => str.split("{")[0].split(")")[0].split("(").at(-1); | |
if (content.startsWith("class ")) { | |
const ctr = content.split("constructor")[1]; | |
if (!ctr) { | |
return parseSignature(Object.getPrototypeOf(token)); | |
} | |
signature = extract(content.split("constructor")[1]); | |
} else { | |
signature = extract(content.split("=>")[0]); | |
} | |
return signature | |
.split(",") | |
.map(param => param.trim()) | |
.filter(Boolean); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {Provider} from "./interfaces"; | |
export function registerProvider(providerOpts: Provider) { | |
providersRegistry.set(providerOpts.token, providerOpts); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class TestUtils { | |
static injector: InjectorService; | |
static async create(locals: { token: TokenProvider, use: any }[] = []) { | |
TestUtils.injector = new InjectorService(); | |
const map = locals.reduce((locals, { token, use }) => { | |
return locals.set(token, use); | |
}, new Map<TokenProvider, any>()); | |
await TestUtils.injector.load(map); | |
} | |
static async invoke<T : any>(token: TokenProvider, locals: { token: TokenProvider, use: any }[] = []): Promise<T> { | |
const map = locals.reduce((locals, { token, use }) => { | |
return locals.set(token, use); | |
}, new Map<TokenProvider, any>()); | |
return TestUtils.injector.invoke(token, map, {rebuild: true}); | |
} | |
static async reset() { | |
const injector = new InjectorService(); | |
await injector.load(); | |
return injector; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage
The current code example, is an extract of the
@tsed/di
without Ts.ED framework specificity. So this example can be reused in any project and can be customized to feat your needs. The idea is also to understand the mechanics of the DI and its possible variants (see the different use cases).The DI implement support the followings features:
Usage with class (explicit dependencies)
This is the most simple usage, but it's declarative.
Usage with class using TS decorators
This version use the Reflect metadata to introspect dependencies.
Usage with class using JS decorators
This version use a factory to get the
Injectable
andInject
decorators. This step is necessary in order to create a context for the class and overcome the lack of metadata carried by the official implementation proposed by the TC39.Usage with factory (explicit dependencies)
Factory let developer to use a function to build the service.
Usage with explicit declaration:
Usage with TS decorators:
Usage with factory (implicit dependencies)
This version use the "angular.js aka Angular 1" principle. The DI will parse the function signature to detect the
dependencies
.Declare async factory
Async factory allow declaring function that build something asynchronous (like a db connection).
Load injector
Hooks
Hooks allows event emitting across injectable service. This pattern let developer to extend his API functionality by implement a hook collection and create his own app lifecycle.
For example a basic usage, is to create a database connection and destroy it when the app is shutdown. With an async factory implemented in our DI, it can be done by listening
$onDestroy
hook:His equivalent with a class:
Testing
This the most advantage of this pattern. Your DI must implement a
TestUtils
to facilitate testing and create mock instances. Using a DI without this part would be nonsense.Here is simple example on how to mocking dependencies using the DI and TestUtils:
PlatformTest.create
create an injector sandbox which will be cleaned a this end of the test usingPlatformTest.reset
. It's really important to work in a sandbox and clean it later to retrieve a stable and fresh test context.