Added a separate factory function for non-class components

This commit is contained in:
Josh Goldberg
2019-09-15 15:18:27 -04:00
parent 859dfc4f13
commit 8d8f11435a
4 changed files with 128 additions and 116 deletions
+15 -5
View File
@@ -11,7 +11,7 @@ BabyIoC is the smallest IoC container you'll ever see _(under 50 lines of code!)
It's also got the fewest toys - it's only targeted for use by [GameStartr](https://github.com/FullScreenShenanigans/GameStartr).
Key tenants:
* All `@components` are members of the container class instance.
* All `@component`s are members of the container class instance.
* Components are stored as lazily evaluated getters: circular dependencies are fine!
* Use TypeScript.
@@ -37,6 +37,8 @@ Components receive the instance of the container as a single constructor paramet
They can use it to reference other components.
```typescript
import { component } from "babyioc";
class DependencyA { }
class DependencyB {
@@ -63,7 +65,11 @@ Your components don't have to be direct classes with dependencies.
Pass functions that take in your container as an argument.
The values returned by those functions are used as the component value.
Use `factory` instead of `component` for these.
```typescript
import { factory } from "babyioc";
class DependencyA {
public constructor(
public readonly member: string,
@@ -73,7 +79,7 @@ class DependencyA {
const createDependencyA = () => new DependencyA("value");
class Container {
@component(createDependencyA)
@factory(createDependencyA)
public readonly dependencyA: DependencyA;
}
@@ -83,6 +89,8 @@ const { dependencyA } = new Container();
These factory functions have access to all the values on the container, including computed getters.
```typescript
import { factory } from "babyioc";
class DependencyA {
public constructor(
public readonly memberA: string,
@@ -100,10 +108,10 @@ const createDependencyA = () => new DependencyA("valueA");
const createDependencyB = (instance: Container) => new DependencyB(dependencyA, container.valueC);
class Container {
@component(createDependencyA)
@factory(createDependencyA)
public readonly dependencyA: DependencyA;
@component(createDependencyB)
@factory(createDependencyB)
public readonly dependencyB: DependencyB;
public readonly valueC = "valueC";
@@ -116,13 +124,15 @@ const { dependencyA, dependencyB } = new Container();
## Technical Details
Marking a member as a `@component` creates a double-layer getter on the class prototype.
Marking a member with `@component` or `@factory` creates a double-layer getter on the class prototype.
The prototype will have a getter defined that writes a getter on the calling object.
Both getters return a new instance of the component.
For example, with this component:
```typescript
import { component } from "babyioc";
class Dependency { }
class Container {
+94 -92
View File
@@ -1,8 +1,8 @@
import { expect } from "chai";
import { component } from "./index";
import { component, factory } from "./index";
// tslint:disable completed-docs no-use-before-declare
// tslint:disable completed-docs max-classes-per-file no-parameter-properties
describe("container", () => {
it("resolves a component dependency", () => {
@@ -78,96 +78,6 @@ describe("container", () => {
expect(dependencyB).to.be.instanceOf(DependencyB);
});
it("creates a component using a factory", () => {
// Arrange
class Dependency {
public constructor(
public readonly member: string,
) { }
}
const memberValue = "memberValue";
const createDependency = () => new Dependency(memberValue);
class Container {
@component(createDependency)
public readonly dependency: Dependency;
}
// Act
const { dependency } = new Container();
// Assert
expect(dependency.member).to.be.equal(memberValue);
});
it("creates different components using factories and their naming classes", () => {
// Arrange
class DependencyA {
public constructor(
public readonly memberA: string,
) { }
}
class DependencyB {
public constructor(
public readonly memberB: string,
) { }
}
const memberValueA = "memberValueA";
const memberValueB = "memberValueB";
const createDependencyA = () => new DependencyA(memberValueA);
const createDependencyB = () => new DependencyB(memberValueB);
class Container {
@component(createDependencyA)
public readonly dependencyA: DependencyA;
@component(createDependencyB)
public readonly dependencyB: DependencyB;
}
// Act
const { dependencyA, dependencyB } = new Container();
// Assert
expect(dependencyA.memberA).to.be.equal(memberValueA);
expect(dependencyB.memberB).to.be.equal(memberValueB);
});
it("passes the container after creating getters to factories", () => {
// Arrange
class DependencyA {
public constructor(
public readonly memberA: string,
) { }
}
class DependencyB {
public constructor(
public readonly referenceA: DependencyA,
public readonly valueC: string,
) { }
}
const memberValueA = "memberValueA";
const createDependencyA = () => new DependencyA(memberValueA);
const createDependencyB = (instance: Container) => new DependencyB(dependencyA, instance.valueC);
class Container {
@component(createDependencyA)
public readonly dependencyA: DependencyA;
@component(createDependencyB)
public readonly dependencyB: DependencyB;
public readonly valueC: string;
}
// Act
const { dependencyA, dependencyB } = new Container();
// Assert
expect(dependencyA.memberA).to.be.equal(memberValueA);
expect(dependencyB.referenceA).to.be.equal(dependencyA);
});
it("allows access to created components in class constructors", () => {
// Arrange
class Dependency { }
@@ -250,3 +160,95 @@ describe("container", () => {
expect(grandChild).to.be.instanceOf(GrandChild);
});
});
describe("factory", () => {
it("creates a component using a factory", () => {
// Arrange
class Dependency {
public constructor(
public readonly member: string,
) { }
}
const memberValue = "memberValue";
const createDependency = () => new Dependency(memberValue);
class Container {
@factory(createDependency)
public readonly dependency: Dependency;
}
// Act
const { dependency } = new Container();
// Assert
expect(dependency.member).to.be.equal(memberValue);
});
it("creates different components using factories and their naming classes", () => {
// Arrange
class DependencyA {
public constructor(
public readonly memberA: string,
) { }
}
class DependencyB {
public constructor(
public readonly memberB: string,
) { }
}
const memberValueA = "memberValueA";
const memberValueB = "memberValueB";
const createDependencyA = () => new DependencyA(memberValueA);
const createDependencyB = () => new DependencyB(memberValueB);
class Container {
@factory(createDependencyA)
public readonly dependencyA: DependencyA;
@factory(createDependencyB)
public readonly dependencyB: DependencyB;
}
// Act
const { dependencyA, dependencyB } = new Container();
// Assert
expect(dependencyA.memberA).to.be.equal(memberValueA);
expect(dependencyB.memberB).to.be.equal(memberValueB);
});
it("passes the container after creating getters to factories", () => {
// Arrange
class DependencyA {
public constructor(
public readonly memberA: string,
) { }
}
class DependencyB {
public constructor(
public readonly referenceA: DependencyA,
public readonly valueC: string,
) { }
}
const memberValueA = "memberValueA";
const createDependencyA = () => new DependencyA(memberValueA);
const createDependencyB = (instance: Container) => new DependencyB(dependencyA, instance.valueC);
class Container {
@factory(createDependencyA)
public readonly dependencyA: DependencyA;
@factory(createDependencyB)
public readonly dependencyB: DependencyB;
public readonly valueC: string;
}
// Act
const { dependencyA, dependencyB } = new Container();
// Assert
expect(dependencyA.memberA).to.be.equal(memberValueA);
expect(dependencyB.referenceA).to.be.equal(dependencyA);
});
});
+19 -10
View File
@@ -1,29 +1,30 @@
export type IClassWithArg<TContainer, TInstance> = new(arg: TContainer) => TInstance;
export type IClassWithArg<TContainer, TInstance> = new (arg: TContainer) => TInstance;
export type IClassWithoutArgs<TInstance> = new() => TInstance;
export type IClassWithoutArgs<TInstance> = new () => TInstance;
export type IComponentFunction<TContainer, TInstance> = (container: TContainer) => TInstance;
export type IComponentClassOrFunction<TContainer, TInstance> =
export type IComponentClass<TContainer, TInstance> =
| IClassWithArg<TContainer, TInstance>
| IClassWithoutArgs<TInstance>
| IComponentFunction<TContainer, TInstance>
;
;
/**
* Adds a member component to a parent container.
* Decorates a caching getter on a class prototype.
*
* @param componentFunction Class or function that creates the component.
* @param factory Method used once within the getter to create an instance member.
*/
export const component = <TContainer extends {}, TInstance>(componentFunction: IComponentClassOrFunction<TContainer, TInstance>) =>
export const factory = <TContainer extends {}, TInstance>(
factory: IComponentFunction<TContainer, TInstance>,
) =>
(parentPrototype: TContainer, memberName: string) => {
Object.defineProperty(parentPrototype, memberName, {
configurable: true,
get(this: TContainer): TInstance {
const value: TInstance = new (componentFunction as IClassWithArg<TContainer, TInstance>)(this);
const value: TInstance = factory(this);
Object.defineProperty(this, memberName, {
configurable: true,
configurable: false,
get: () => value,
});
@@ -31,3 +32,11 @@ export const component = <TContainer extends {}, TInstance>(componentFunction: I
},
});
};
/**
* Decorates a member component class on a class prototype.
*
* @param componentClass Class to be initialized for the member instance.
*/
export const component = <TContainer extends {}, TInstance>(componentClass: IComponentClass<TContainer, TInstance>) =>
factory((container: TContainer) => new componentClass(container));
-9
View File
@@ -4,14 +4,5 @@
"exclude": [
"./node_modules/**/*"
]
},
"rules": {
"ban-types": false,
"completed-docs": false,
"max-classes-per-file": [true, "exclude-class-expressions"],
"no-non-null-assertion": false,
"no-parameter-properties": false,
"only-arrow-functions": false,
"no-shadowed-variable": false
}
}