babyioc

Infantile IoC decorator with almost no features.

Usage no npm install needed!

<script type="module">
  import babyioc from 'https://cdn.skypack.dev/babyioc';
</script>

README

BabyIoc

Code Style: Prettier TypeScript: Strict NPM version Join the chat at https://gitter.im/FullScreenShenanigans/community

Infantile IoC decorator with almost no features.

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 EightBittr.

Key tenants:

  • All @members are literally members of the container class instance.
  • Members are stored as lazily evaluated getters: circular dependencies are fine!
  • Use TypeScript.

Usage

Each @member is a literal member of your container class. Declare your members with their classes to have them automagically created as members of your class.

import { member } from "babyioc";

class DependencyA {}

class Container {
    @member(DependencyA)
    public readonly dependencyA: DependencyA;
}

const { dependencyA } = new Container();

Members receive the instance of the container as a single constructor parameter. They can use it to reference other members.

import { member } from "babyioc";

class DependencyA {}

class DependencyB {
    public constructor(public readonly instance: Container) {}
}

class Container {
    @member(DependencyA)
    private readonly dependencyA: DependencyA;

    @member(DependencyB)
    public readonly dependencyB: DependencyB;
}

const { dependencyB } = new Container();
const { dependencyA } = depdendencyB.instance;

Factories

Your members 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 member value.

Use factory instead of member for these.

import { factory } from "babyioc";

class DependencyA {
    public constructor(public readonly member: string) {}
}

const createDependencyA = () => new DependencyA("value");

class Container {
    @factory(createDependencyA)
    public readonly dependencyA: DependencyA;
}

const { dependencyA } = new Container();

These factory functions have access to all the values on the container, including computed getters.

import { factory } from "babyioc";

class DependencyA {
    public constructor(public readonly memberA: string) {}
}
class DependencyB {
    public constructor(public readonly referenceA: DependencyA, public readonly valueC: string) {}
}

const createDependencyA = () => new DependencyA("valueA");

const createDependencyB = (instance: Container) => new DependencyB(dependencyA, container.valueC);

class Container {
    @factory(createDependencyA)
    public readonly dependencyA: DependencyA;

    @factory(createDependencyB)
    public readonly dependencyB: DependencyB;

    public readonly valueC = "valueC";
}

const { dependencyA, dependencyB } = new Container();

...and that's about it!

Technical Details

Marking a member with @member 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 member.

For example, with this member:

import { member } from "babyioc";

class Dependency {}

class Container {
    @member(Dependency)
    public readonly myDependency: Dependency;
}

Container.prototype has a getter defined on "myDependency" that creates a new Dependency(this) and writes a getter on the calling scope's "myDependency" to return it. In practical use, that means the first getter will stay on Container.prototype, and the calling scope that receives the second getter will generally be an instance of the Container class.

See index.ts.

Development

This repository is a portion of the EightBittr monorepo. See its docs/Development.md for details on how to get started. 💖

Running Tests

yarn run test

Tests are written in Mocha and Chai. Their files are written using alongside source files under src/ and named *.test.ts?. Whenever you add, remove, or rename a *.test.t* file under src/, watch will re-run yarn run test:setup to regenerate the list of static test files in test/index.html. You can open that file in a browser to debug through the tests, or run yarn test:run to run them in headless Chrome.

Philosophy

Is BabyIoC an IoC framework?

If you consider the Container classes from the samples to be equivalent to IoC containers à la Inversify, then sure. The main difference is that members are encouraged to have knowledge of the full application type instead of just their dependencies.

Is BabyIoC a good IoC framework?

Lol, no.

Application members generally shouldn't have knowledge of the full application. BabyIoC also has almost no features. You should probably use something standard like Inversify.

Does BabyIoC violate SOLID principles?

Debatably no.

There's nothing inherently non-SOLID in members being passed the root IoC container. Such an actor happens behind the scenes in normal IoC frameworks; BabyIoC members just don't have the layer of indirection given by declaring only required parameters. Just as BabyIoC members can access anyactor they want, so too can traditional classes by taking in an obscene number of dependencies.