@lillallol/dic
v2.0.0
Published
My own dependency injection container.
Downloads
11
Maintainers
Readme
DIC
Table of contents
- Table of contents
- Installation
- Description
- Code coverage
- Examples
- Documentation
- Motivation
- Acknowledgments
- Contributing
- Changelog
- License
Installation
npm install @lillallol/dic
Description
A dependency injection container (DIC) with the following characteristics:
- configuration as code (no auto wiring)
- there will be helpful error messages when a registration has missing or extra dependencies
- only factory (i.e. functions) registrations
- singleton and transient lifecycle (no scoped lifecycle)
- interception at composition
- ecmascript symbols for interfaces
- manual injection on object composition
- state reset for memoized concretions of singleton lifecycle
- abstraction un-registration
Utility functions are provided that:
- locate circular loops in the dependency graph
- find dead registrations and abstractions
- print the dependency graph
Code coverage
Testing code coverage is around 90%.
Examples
Composition
import { Dic } from "../Dic/Dic";
describe(Dic.name, () => {
it("creates the concretion of the provided abstraction", () => {
/**
* Dependency graph:
*
* ```
* foo
* ↙ ↘
* bar baz
* ```
*/
const dic = new Dic();
const TYPES = {
foo: Symbol("foo"),
bar: Symbol("bar"),
baz: Symbol("baz"),
};
type interfaces = {
foo: (x: number) => number;
bar: () => number;
baz: () => number;
};
function fooFactory(bar: interfaces["bar"], baz: interfaces["baz"]): interfaces["foo"] {
return function foo(x) {
return bar() + baz() + x;
};
}
function barFactory(): interfaces["bar"] {
return function bar() {
return 1;
};
}
function bazFactory(): interfaces["baz"] {
return function baz() {
return -1;
};
}
dic.register({
abstraction: TYPES.foo,
dependencies: [TYPES.bar, TYPES.baz],
factory: fooFactory,
lifeCycle: "transient",
});
dic.register({
abstraction: TYPES.bar,
dependencies: [],
factory: barFactory,
lifeCycle: "singleton",
});
dic.register({
abstraction: TYPES.baz,
dependencies: [],
factory: bazFactory,
lifeCycle: "singleton",
});
const foo: interfaces["foo"] = dic.get({ abstraction: TYPES.foo });
expect(foo(0)).toBe(1 + -1 + 0);
});
});
Manual injection
import { Dic } from "../Dic/Dic";
describe(Dic, () => {
it("manually injects the provided concretion", () => {
/**
* Dependency graph:
*
* ```
* a
* ↙ ↘
* b c
* ```
*/
const dic = new Dic();
const TYPES = {
a: Symbol("a"),
b: Symbol("b"),
c: Symbol("c"),
};
type interfaces = {
a: number;
b: number;
c: number;
};
function aFactory(b: interfaces["b"], c: interfaces["c"]): interfaces["a"] {
return b + c;
}
function bFactory(): interfaces["b"] {
return 1;
}
function cFactory(): interfaces["c"] {
return -1;
}
dic.register({
abstraction: TYPES.a,
dependencies: [TYPES.b, TYPES.c],
factory: aFactory,
lifeCycle: "transient",
});
dic.register({
abstraction: TYPES.b,
dependencies: [],
factory: bFactory,
lifeCycle: "singleton",
});
dic.register({
abstraction: TYPES.c,
dependencies: [],
factory: cFactory,
lifeCycle: "singleton",
});
const inject = new Map([[TYPES.c, -2]]);
expect(dic.get({ abstraction: TYPES.a, inject })).toBe(1 + -2);
});
});
Print dependency graph
import { Dic, printDependencyGraph } from "../";
import { tagUnindent } from "../es-utils/tagUnindent";
describe(printDependencyGraph.name, () => {
it("prints the dependency graph", () => {
/**
* Dependency graph:
*
* ```
* a
* ↙ ↘
* b c
* ```
*/
const dic = new Dic();
const TYPES = {
a: Symbol("a"),
b: Symbol("b"),
c: Symbol("c"),
};
type interfaces = {
a: void;
b: void;
c: void;
};
function aFactory(b: interfaces["b"], c: interfaces["c"]): interfaces["a"] {
b; //use b somehow
c; //use c somehow
return;
}
function bFactory(): interfaces["b"] {
return;
}
function cFactory(): interfaces["c"] {
return;
}
dic.register({
abstraction: TYPES.a,
dependencies: [TYPES.b, TYPES.c],
factory: aFactory,
lifeCycle: "singleton",
});
dic.register({
abstraction: TYPES.b,
dependencies: [],
factory: bFactory,
lifeCycle: "singleton",
});
dic.register({
abstraction: TYPES.c,
dependencies: [],
factory: cFactory,
lifeCycle: "singleton",
});
expect(printDependencyGraph({ TYPES, dic, rootAbstraction: TYPES.a })).toBe(tagUnindent`
total number of unique components: 3
a
|_ b
|_ c
`);
});
});
Dead registrations
import { Dic } from "../Dic/Dic";
import { tagUnindent } from "../es-utils/tagUnindent";
import { validateDependencyGraph } from "../validateDependencyGraph/validateDependencyGraph";
describe(validateDependencyGraph.name, () => {
it("throws when the combined entry point abstractions not cover the whole dependency graph", () => {
/**
* Dependency graph:
*
* ```
* a d
* ↙ ↘ ↙
* b c
* ```
*
* Entry point abstractions:
*
* a
*
* Dead abstraction:
*
* d
*
*/
const dic = new Dic();
const TYPES = {
a: Symbol("a"),
b: Symbol("b"),
c: Symbol("c"),
d: Symbol("d"),
};
type interfaces = {
a: void;
b: void;
c: void;
d: void;
};
function aFactory(b: interfaces["b"], c: interfaces["c"]): interfaces["a"] {
b; //use b somehow
c; //use c somehow
return;
}
function bFactory(): interfaces["b"] {
return;
}
function cFactory(): interfaces["c"] {
return;
}
function dFactory(c: interfaces["c"]): interfaces["d"] {
c; //use c somehow
return;
}
dic.register({
abstraction: TYPES.a,
dependencies: [TYPES.b, TYPES.c],
factory: aFactory,
lifeCycle: "transient",
});
dic.register({
abstraction: TYPES.b,
dependencies: [],
factory: bFactory,
lifeCycle: "singleton",
});
dic.register({
abstraction: TYPES.c,
dependencies: [],
factory: cFactory,
lifeCycle: "singleton",
});
dic.register({
abstraction: TYPES.d,
dependencies: [TYPES.c],
factory: dFactory,
lifeCycle: "singleton",
});
expect(() =>
validateDependencyGraph({
TYPES,
dic,
entryPointAbstractions: [TYPES.a],
})
).toThrow(tagUnindent`
The following abstractions:
Symbol(d)
are not used by the entry point abstractions:
Symbol(a)
`);
});
});
Graph cycles
import { validateDependencyGraph } from "../";
import { Dic } from "../Dic/Dic";
import { tagUnindent } from "../es-utils/tagUnindent";
describe(validateDependencyGraph.name, () => {
it("detects circular loops in the dependency graph", () => {
/**
* Dependency graph:
*
* ```
* a ← c
* ↘ ↗
* b
* ```
*
* Entry point abstraction:
*
* a
*
*/
const dic = new Dic();
const TYPES = {
a: Symbol("a"),
b: Symbol("b"),
c: Symbol("c"),
};
const entryPointAbstractions = [TYPES.a];
type interfaces = {
a: void;
b: void;
c: void;
};
function aFactory(b: interfaces["b"]): interfaces["a"] {
b; //use b somehow
}
function bFactory(c: interfaces["c"]): interfaces["b"] {
c; //use c somehow
}
function cFactory(a: interfaces["a"]): interfaces["c"] {
a; //use a somehow
}
dic.register({
abstraction: TYPES.a,
dependencies: [TYPES.b],
factory: aFactory,
lifeCycle: "singleton",
});
dic.register({
abstraction: TYPES.b,
dependencies: [TYPES.c],
factory: bFactory,
lifeCycle: "singleton",
});
dic.register({
abstraction: TYPES.c,
dependencies: [TYPES.a],
factory: cFactory,
lifeCycle: "singleton",
});
expect(() =>
validateDependencyGraph({
dic,
entryPointAbstractions,
TYPES,
})
).toThrow(tagUnindent`
The composition graph of:
Symbol(a)
has a cycle on the following path:
┌> Symbol(a)
│ ↓
│ Symbol(b)
│ ↓
└─ Symbol(c)
`);
});
});
Interception
import { Dic } from "../Dic/Dic";
describe(Dic.name, () => {
it("allows interception", () => {
const dic = new Dic();
const TYPES = {
a: Symbol("a"),
};
type interfaces = {
a: (x1: number, x2: number) => number;
};
function aFactory(): interfaces["a"] {
return function a(x1, x2) {
return x1 + x2;
};
}
dic.register(
{
abstraction: TYPES.a,
dependencies: [],
factory: aFactory,
lifeCycle: "singleton",
},
{
intercept: [
({ concretion }) => {
return function a(x1, x2) {
if (typeof x1 !== "number") throw Error("`x1` has to be of type number.");
if (typeof x2 !== "number") throw Error("`x2` has to be of type number.");
return concretion(x1, x2);
};
},
],
}
);
const a: interfaces["a"] = dic.get({ abstraction: TYPES.a });
//@ts-expect-error
expect(() => a("0", 1)).toThrow();
});
});
AOP
You do aspect oriented programming (AOP), when cross cutting concerns (CCC) are applied in a centralized and DRY way:
import { Dic } from "../Dic/Dic";
describe(Dic.name, () => {
it("enables AOP via interception", () => {
const dic = new Dic();
const TYPES = {
foo: Symbol("foo"),
bar: Symbol("bar"),
baz: Symbol("baz"),
};
type interfaces = {
foo: () => void;
bar: () => void;
baz: () => void;
};
function fooFactory(bar: interfaces["bar"], baz: interfaces["baz"]): interfaces["foo"] {
return function foo() {
bar();
baz();
return;
};
}
function barFactory(): interfaces["bar"] {
return function bar() {
return;
};
}
function bazFactory(): interfaces["baz"] {
return function baz() {
return;
};
}
dic.register({
abstraction: TYPES.foo,
dependencies: [TYPES.bar, TYPES.baz],
factory: fooFactory,
lifeCycle: "transient",
});
dic.register({
abstraction: TYPES.bar,
dependencies: [],
factory: barFactory,
lifeCycle: "singleton",
});
dic.register({
abstraction: TYPES.baz,
dependencies: [],
factory: bazFactory,
lifeCycle: "singleton",
});
const callStack: string[] = [];
dic.registry.forEach((registration) => {
registration.intercept.push(({ concretion }) => {
if (typeof concretion === "function") {
return (...args: unknown[]) => {
callStack.push(concretion.name);
return concretion(...args);
};
}
return concretion;
});
});
const foo: interfaces["foo"] = dic.get({ abstraction: TYPES.foo });
foo();
expect(callStack).toEqual(["foo", "bar", "baz"]);
});
});
Documentation
/**
* @description
* Dependency injection container constructor.
*/
export declare const Dic: DicCtor;
export declare type DicCtor = new () => IDic;
export declare type IDic = {
/**
* @description
* Maps abstractions to their corresponding registrations.
*/
registry: Map<
symbol,
{
abstraction: symbol;
dependencies: symbol[];
factory: Function;
lifeCycle: "singleton" | "transient";
intercept: ((parameters: { dic: IDic; concretion: any }) => any)[];
}
>;
/**
* @description
* All abstractions that have been `get`ed and have singleton lifecycle are
* memoized in this memoization table.
*/
memoizationTable: Map<symbol, unknown>;
/**
* @description
* Deletes all the memoized values from the memoization table.
*/
clearMemoizationTable: () => void;
/**
* @description
* Adds a registration to the dic.
*/
register: <P extends unknown[], R>(
arg0: {
abstraction: symbol;
dependencies: symbol[];
factory: (...args: P) => R;
lifeCycle: "singleton" | "transient";
},
arg1?: {
intercept?: ((parameters: { dic: IDic; concretion: R }) => R)[];
}
) => void;
/**
* @description
* Deletes the registration of the provided abstraction from the registry.
* It returns `true` if the abstraction registration was found and got
* deleted, and `false` if it was not found.
*/
unregister: (parameters: {
/**
* @description
* Abstraction to unregister from the registry.
*/
abstraction: symbol;
}) => boolean;
/**
* @description
* Returns the concretion of the provided abstraction.
*/
get: <T>(parameters: {
/**
* @description
* The abstraction for which you want to get the concretion. Make sure
* that the symbol is defined with a name (e.g `Symbol("my-name")`) so
* that more helpful error messages are given.
*/
abstraction: symbol;
/**
* @description
* Provide manual concretions to be injected when the abstraction
* dependency graph is composed.
*
* The already memoized values override the provided injection values.
*/
inject?: Map<symbol, unknown>;
}) => T;
};
/**
* @description
* It returns a string representation of the dependency graph starting from the
* provided abstraction.
*/
export declare const printDependencyGraph: (parameters: {
dic: IDic;
rootAbstraction: symbol;
TYPES: ITYPES;
}) => string;
export declare type IDic = {
/**
* @description
* Maps abstractions to their corresponding registrations.
*/
registry: Map<
symbol,
{
abstraction: symbol;
dependencies: symbol[];
factory: Function;
lifeCycle: "singleton" | "transient";
intercept: ((parameters: { dic: IDic; concretion: any }) => any)[];
}
>;
/**
* @description
* All abstractions that have been `get`ed and have singleton lifecycle are
* memoized in this memoization table.
*/
memoizationTable: Map<symbol, unknown>;
/**
* @description
* Deletes all the memoized values from the memoization table.
*/
clearMemoizationTable: () => void;
/**
* @description
* Adds a registration to the dic.
*/
register: <P extends unknown[], R>(
arg0: {
abstraction: symbol;
dependencies: symbol[];
factory: (...args: P) => R;
lifeCycle: "singleton" | "transient";
},
arg1?: {
intercept?: ((parameters: { dic: IDic; concretion: R }) => R)[];
}
) => void;
/**
* @description
* Deletes the registration of the provided abstraction from the registry.
* It returns `true` if the abstraction registration was found and got
* deleted, and `false` if it was not found.
*/
unregister: (parameters: {
/**
* @description
* Abstraction to unregister from the registry.
*/
abstraction: symbol;
}) => boolean;
/**
* @description
* Returns the concretion of the provided abstraction.
*/
get: <T>(parameters: {
/**
* @description
* The abstraction for which you want to get the concretion. Make sure
* that the symbol is defined with a name (e.g `Symbol("my-name")`) so
* that more helpful error messages are given.
*/
abstraction: symbol;
/**
* @description
* Provide manual concretions to be injected when the abstraction
* dependency graph is composed.
*
* The already memoized values override the provided injection values.
*/
inject?: Map<symbol, unknown>;
}) => T;
};
export declare type ITYPES = {
[x: string]: symbol;
};
/**
* @description
* Provide `TYPES` to get back an identity function that provides intellisense
* for the keys of `TYPES`. This function can be used to have refactor-able
* names in the specification of unit tests.
*/
export declare const namesFactory: <T extends ITYPES>() => <
N extends keyof T
>(
name: N
) => N;
export declare type ITYPES = {
[x: string]: symbol;
};
/**
* @description
* It throws error when:
*
* * the dependency graph of the provided entry abstractions
* does not use all the registered abstractions
* * `TYPES` has extra or missing abstractions
* * there are cycles in the dependency graph
*
*/
export declare const validateDependencyGraph: (parameters: {
dic: IDic;
entryPointAbstractions: symbol[];
TYPES: ITYPES;
ignoreAbstractions?: symbol[] | undefined;
}) => void;
export declare type IDic = {
/**
* @description
* Maps abstractions to their corresponding registrations.
*/
registry: Map<
symbol,
{
abstraction: symbol;
dependencies: symbol[];
factory: Function;
lifeCycle: "singleton" | "transient";
intercept: ((parameters: { dic: IDic; concretion: any }) => any)[];
}
>;
/**
* @description
* All abstractions that have been `get`ed and have singleton lifecycle are
* memoized in this memoization table.
*/
memoizationTable: Map<symbol, unknown>;
/**
* @description
* Deletes all the memoized values from the memoization table.
*/
clearMemoizationTable: () => void;
/**
* @description
* Adds a registration to the dic.
*/
register: <P extends unknown[], R>(
arg0: {
abstraction: symbol;
dependencies: symbol[];
factory: (...args: P) => R;
lifeCycle: "singleton" | "transient";
},
arg1?: {
intercept?: ((parameters: { dic: IDic; concretion: R }) => R)[];
}
) => void;
/**
* @description
* Deletes the registration of the provided abstraction from the registry.
* It returns `true` if the abstraction registration was found and got
* deleted, and `false` if it was not found.
*/
unregister: (parameters: {
/**
* @description
* Abstraction to unregister from the registry.
*/
abstraction: symbol;
}) => boolean;
/**
* @description
* Returns the concretion of the provided abstraction.
*/
get: <T>(parameters: {
/**
* @description
* The abstraction for which you want to get the concretion. Make sure
* that the symbol is defined with a name (e.g `Symbol("my-name")`) so
* that more helpful error messages are given.
*/
abstraction: symbol;
/**
* @description
* Provide manual concretions to be injected when the abstraction
* dependency graph is composed.
*
* The already memoized values override the provided injection values.
*/
inject?: Map<symbol, unknown>;
}) => T;
};
export declare type ITYPES = {
[x: string]: symbol;
};
Motivation
Made for learning purposes but ended up using it in my own projects, so I decided to publish it to npm.
Acknowledgments
The following resources had a detrimental role in the creation of this module:
Contributing
I am open to suggestions/pull request to improve this program.
You will find the following commands useful:
Clones the github repository of this project:
git clone https://github.com/lillallol/dic
Installs the node modules (nothing will work without them):
npm install
Tests the source code:
npm run test
Lints the source folder using typescript and eslint:
npm run lint
Builds the typescript code from the
./src
folder to javascript code in./dist
:npm run build-ts
Injects in place the generated toc and imported files to
README.md
:npm run build-md
Checks the project for spelling mistakes:
npm run spell-check
Take a look at the related configuration
./cspell.json
.Checks
./src
for dead typescript files:npm run dead-files
Take a look at the related configuration
./unimportedrc.json
.Logs in terminal which
dependencies
anddevDependencies
have a new version published in npm:npm run check-updates
Updates the
dependencies
anddevDependencies
to their latest version:npm run update
Formats all
.ts
files of the./src
folder:npm run format
Changelog
2.0.0
breaking changes
Symbols that are used for abstractions have to be defined with a name. For example:
const TYPES = { myAbstraction: Symbol("myAbstraction"), };
This is done to have more helpful error messages.
The
intercept
argument ofdic.get
is now on its own object in a second optional argument. This was done to avoid limitations in type inference:Old:
No linting errors for trivial interception:
dic.register( { abstraction: Symbol("A"), dependencies: [], factory: function A(): () => number { return (): number => 1; }, lifeCycle: "singleton", }, { intercept: [ ({ concretion }) => { return concretion; }, ], } );
Lints error for non trivial interception:
dic.register({ abstraction: Symbol("A"), dependencies: [], factory: function A(): () => number { return (): number => 1; }, lifeCycle: "singleton", intercept: [ ({ concretion }) => { // lints error here return () => concretion(); }, ], });
New:
No linting errors for non trivial interception:
dic.get( { abstraction: Symbol("A"), dependencies: [], factory: function A(): () => number { return (): number => 1; }, lifeCycle: "singleton", }, { intercept: [ ({ concretion }) => { return () => concretion(); }, ], } );
notice that
get
now receives two parameters instead of single one.throwIfDeadRegistrations
has been renamed tovalidateDependencyGraph
. It now hasTYPES
as required parameter.That is because it finds extra or missing abstractions ofTYPES
object. It also detects circular loops in the dependency graph. Finally you can specify those abstractions that are correctly not used by your entry point abstractions via the parameterignoreAbstractions
.printDependencyTree
has been renamed toprintDependencyGraph
.Factories that are registered have to have a name property that is of non zero length and not equal to string
"factory"
. This is done to have more helpful error messages.The properties
_memoizationTable
and_registry
ofDic
instances have been renamed tomemoizationTable
andregistry
respectively.Registrations no longer have property
hasBeenMemoized
.
Other
- Added sections Contributing, Changelog, Code coverage, in
README.md
. - Added actual documentation in the Documentation section of
README.md
.
1.1.0
non breaking changes
- Added function
throwIfDeadRegistrations
which throws error when there are dead registrations in the dic.
other
- Added
CHANGELOG.md
.
1.0.0
- Published the package.
License
MIT