Question

Say we have a class structure like the following:

// implementation.ts
export class A implements IA {
    constructor(private b: IB) {}
}
export class B implements IB {
    constructor(private c: IC) {}
}
export class C implements IC {
}
// ... interface declarations

Using an IoC container, it is possible to inject dependencies using annotations like @Inject or @AutoWired, however I believe there are two caveats:

  • Using annotations adds further concerns to my classes. A, B and C should only be responsible for the implementation and not for what should be implemented in which manner.
  • We risk to encounter runtime errors because the IoC container does not support compile-time checks whether each dependency is met.

So I thought a bit and came up with an additional layer responsible for dependency injection:

// dependency-injection.ts
let impl = require("./implementation");
export class A extends impl.A {
    constructor() {
        super(new B());
}
export class B extends impl.B {
    constructor() {
        super(new B());
}
export class C extends impl.C {
}

To instatiate A with the injected dependencies in my application on startup, I would simply call new diModule.A(). This solution has many advantages:

  • does not require any framework
  • supports compile-time checks whether a dependency is met
  • separates the concerns of implementation and wiring
  • is very flexible, i.e. does not constrain the way of wiring

...while keeping the advantages of an IoC container which come to my mind:

  • allows sophisticated dependency injection
  • avoids constructor nesting: new A(new B(new C()))
  • when updating a dependency, I have to change one and only one class
  • it is quite easy to apply different implementations for different purposes, for example by having two classes LoggerForWarnings and LoggerForErrors.

Which of these two aspect-oriented approaches would you prefer and why? Is there a name for my approach? Are there further disadvantages of the DI layer or further advantages of an IoC container?


Having decided which answer to accept, I'll try to summarize some aspects of the two answers. I discussed a lot with Adrian Iftode and although I'm still critical about IoC containers without compile-time dependency checks, his arguments were worth a +1 - but unfortunately, I'm missing the reputation.

  • Christian Willman noted that for projects which don't exceed a certain size and complexity, my approach is often realized without all the class foo, simply wiring all the classes by constructor injection at composition root directly.
  • Adrian Iftode remarked that IoC containers can do much more than the classes A, B and C can - for example, they can manage the lifetime of objects or provide singletons. This functionality could be added to my example by using factories instead of constructors in the DI layer.
  • What I'm still wondering about is if the approach is also useful for larger projects. In my opinion, directly doing constructor injection in main() will be quite difficult to maintain while creating several factories in the DI layer could improve maintainability whilst keeping the advantages of other IoC containers.

Thank you two very much!

Was it helpful?

Solution

You can actually get away with a much simpler implementation. Just manually wire everything together in the so-called composition root, which is just the application's entrypoint.

Of course you need to be mindful of the dependency resolution order and first instantiate the classes which themselves have no dependencies (that is, the sinks in the object dependency graph).

In your example:

function main() {
  const c: C = new C();
  const b: B = new B(c);
  const a: A = new A(b);
}

The benefits are twofold. You don't clutter your classes with default constructors, and the (brutally) straightforward instantiation process makes the code flow obvious to maintainers.

OTHER TIPS

You are creating the so-called default constructors. When people start using DI they provide two constructors: one to inject the dependencies and a default one where they create the dependencies. When you are newing a dependency you do a tight coupling with that dependency so this is not dependency inversion anymore.

Licensed under: CC-BY-SA with attribution
scroll top