Simplifying Dependency Injection: A Fresh Approach

What is Dependency Injection?

Dependency Injection (DI) is a design pattern that helps you manage the relationships between components in your application. It’s a way to decouple components from each other, making it easier to test, maintain, and extend your codebase. By using DI, you can focus on writing features instead of worrying about the plumbing of your application.

Why Use Dependency Injection?

There are many benefits to using DI:

  • Separation of Concerns: DI helps you keep each component focused on its own responsibilities, making it easier to develop and maintain.
  • Loose Coupling: With DI, components are no longer tightly coupled, making it easier to change or replace them without affecting the rest of the application.
  • Easier Testing: DI makes it easier to test components in isolation, reducing the complexity of your tests.
  • Flexibility: DI allows you to swap out components or implementations without affecting the rest of the application.

Alternatives to DI

Before we dive into our fresh approach, let’s consider some alternatives to DI:

  • Manual Wiring: You can manually create and wire up components, but this can lead to tight coupling and make your code harder to maintain.
  • Service Locator Pattern: This pattern is similar to DI, but it requires more boilerplate code and can be less flexible.

Introducing Fusion: A Simple DI Library

Fusion is a lightweight DI library that makes it easy to wire up your application’s components. It’s built on top of TypeScript decorators and the JavaScript Proxy class, making it simple to use and extend.

Marking Up Classes for Injection

@InjectableClass
class MyClass {
  //...
}

To use Fusion, you need to mark up your classes with the @InjectableClass decorator. This tells Fusion to create a proxy for the class, allowing it to intercept calls to the constructor and inject dependencies.

Recording Injectable Properties

@InjectableClass
class MyClass {
  @InjectProperty('dependencyId')
  private myDependency: MyDependency;
  //...
}

Next, you need to record the properties that should be injected using the @InjectProperty decorator. This decorator marks the property as injectable and records the dependency ID.

Creating a Proxy Constructor

Fusion uses a proxy constructor to intercept calls to the original constructor and inject dependencies. This allows you to focus on writing your application code without worrying about the plumbing.

Resolving Dependencies

function resolvePropertyDependencies(instance: any): void {
  // Loop over the recorded injectable properties
  // and instantiate the dependencies using the instantiateSingleton function
}

When you instantiate a class, Fusion resolves the dependencies by calling the resolvePropertyDependencies function. This function loops over the recorded injectable properties and instantiates the dependencies using the instantiateSingleton function.

Manually Registering Singletons

registerSingleton('dependencyId', MyDependency, ['constructorArg1', 'constructorArg2']);

In some cases, you may need to manually register singletons using the registerSingleton function. This allows you to specify the implementation and constructor arguments for the singleton.

Automatically Registering Singletons

@InjectableSingleton
class MySingleton {
  //...
}

Fusion also provides an @InjectableSingleton decorator that allows you to mark classes as lazily initialized singletons. This decorator records the constructor and dependency ID, allowing Fusion to instantiate the singleton when it’s needed.

Where Do We Go From Here?

Now that we’ve covered the basics of Fusion, let’s consider some future enhancements:

  • Automated Injection of Constructor Parameters: This would allow you to inject constructor parameters, making it even easier to use DI in your application.
  • Type Safety: You can use tslint to create a rule to enforce type safety, ensuring that your dependencies are correctly typed.

Leave a Reply