allow the use of a class as dependency identifier

This commit is contained in:
Robin Chappatte
2024-05-31 14:50:46 +02:00
parent 46f08d42cc
commit bd7d2aae65
2 changed files with 69 additions and 51 deletions

View File

@@ -1,44 +1,53 @@
type Provider<T> = (manager: DependencyManager) => Promise<T> | T;
type Modules = Map<string, {
type ClassType<T> = new () => T;
// deno-lint-ignore no-explicit-any
type ModuleIdentifier = any;
type Modules = Map<ModuleIdentifier, {
// deno-lint-ignore no-explicit-any
provider: Provider<any>;
// deno-lint-ignore no-explicit-any
instance?: any;
}>;
export class DependencyManager {
private modules: Modules = new Map();
private resolving: Set<string> = new Set();
private resolving: Set<ModuleIdentifier> = new Set();
// Enregistrer un module avec son initialisateur
register<T>(name: string, provider: Provider<T>): void {
if (this.modules.has(name)) {
throw new Error(`Module ${name} is already registered.`);
register<T>(identifier: ModuleIdentifier, provider: Provider<T>): void {
if (this.modules.has(identifier)) {
throw new Error(`Module ${identifier} is already registered.`);
}
this.modules.set(name, { provider });
this.modules.set(identifier, { provider });
}
// Résoudre un module et ses dépendances
async resolve<T>(name: string): Promise<T> {
const module = this.modules.get(name);
async resolve<T>(identifier: ClassType<T>): Promise<T>;
async resolve<T>(identifier: ModuleIdentifier): Promise<T>;
async resolve(identifier: ModuleIdentifier) {
const module = this.modules.get(identifier);
if (!module) {
throw new Error(`Module ${name} is not registered.`);
throw new Error(`Module ${identifier} is not registered.`);
}
if (module.instance) {
return module.instance;
}
if (this.resolving.has(name)) {
throw new Error(`Circular dependency detected for module ${name}.`);
if (this.resolving.has(identifier)) {
throw new Error(`Circular dependency detected for module ${identifier}.`);
}
this.resolving.add(name);
this.resolving.add(identifier);
try {
module.instance = await module.provider(this);
return module.instance;
} finally {
this.resolving.delete(name);
this.resolving.delete(identifier);
}
}
}

View File

@@ -2,14 +2,16 @@
This lib provides a simplistic dependency manager.
## Features and limitations
## Features
- It resolves dependencies only when they are needed
- It detect circular dependency
- Resolves dependencies only when they are needed
- Detect circular dependency
- Accept any valid `Map` key as dependency identifier
- Infer the dependency type from the dependency identifier if the later is a class
## Usage
Create a manager:
1. Create a manager:
```typescript
import { DependencyManager } from 'dependency-manager/mod.ts'
@@ -17,48 +19,55 @@ import { DependencyManager } from 'dependency-manager/mod.ts'
const manager = new DependencyManager()
```
Create providers for your dependencies.
A provider is a function that is called when attempting to retrieve the dependency for the first time.
It receive the dependency manager, and return the dependency value, or a promise that resolves to it.
2. Register dependencies by giving the manager an identifier and a provider:
```typescript
// dependency-free provider
function loggerProvider() {
return console.log
manager.register('dependency-identifier', () => 'value');
```
3. Get the dependency by giving the manager its identifier (always asynchrone):
```typescript
const value = await manager.resolve('dependency-identifier');
```
## Providers
Providers are functions that are called when resolving the dependency for the first time.
They receive the dependency manager as parameter, and should return the dependency value, or a promise that resolves to it.
Example of the registration of a dependency which provider uses another dependency:
```typescript
async function provider(manager: DependencyManager) {
const value = await manager.resolve('dependencyIdentifier');
return `The value is: ${valueA}`;
}
type Logger = ReturnType<typeof loggerProvider>;
manager.register('other-identifier', provider);
```
// provider that need the 'logger' dependency
async function fooProvider(manager: DependencyManager) {
const logger = await manager.resolve<Logger>('logger');
## Identifiers and typing
return {
bar: () => logger('bar'),
baz: () => logger('baz'),
}
Any valid `Map` key can be used as identifier, but using a class allow the return value of the `resolve` method to be automatically typed:
```typescript
class MyDependency {}
manager.register(MyDependency, () => new MyDependency());
const a = await manager.resolve(MyDependecy); //< `a` is of type `MyDependency`
```
Otherwise you can provide the type as type argument:
```typescript
function provider() {
return 'foo'
}
type Foo = Awaited<ReturnType<typeof fooProvider>>;
manager.register('my-dependency-identifier', provider);
const a = await manager.resolve('my-dependency-identifier'); //< `a` is of type `unknown`
const b = await manager.resolve<string>('my-dependency-identifier'); //< `b` is of type `string`
```
Register the dependency by passing the `register` method its name and provider:
```typescript
manager.register('logger', loggerProvider);
manager.register('foo', fooProvider);
```
Retrieve dependencies by resolving them (always asynchrone, even if the provider was synchrone):
```typescript
const foo = await manager.resolve<Foo>('foo');
foo.bar();
foo.baz();
```
## Typing
Types should be explicitly defined when resolving a dependency.
## Errors
The `register` method throw an error if: