Clean constructor dependency injection by using a proxy object

ยท

3 min read

Foreword

This is a refactoring approach that I did when I faced a problem with redundant code brought by dependency injection. I had an abstract class that needs a set of dependencies. Defining the dependencies again and again in the constructor of each subclass seemed tedious. Even more so, the problem was aggravated further when I used the factory pattern to instantiate each subclass. So, I looked back and found a better way!

Problem

Consider the following abstract class called PlayerService. It has a dependency with GameInventoryService and GameWorldService. Normally, and as many are familiar with, one would just leverage using the constructor parameters for dependency injection.

export abstract class PlayerService {
    constructor(
        protected gameInventoryService: GameInventoryService,
        protected gameWorldService: GameWorldService
    ) {}
}

It looks fine on its own! But, imagine two types of player services named OfflinePlayerService and OnlinePlayerService. They would extend the abstract PlayerService class. This can get messy if you are working to instantiate these classes using the factory pattern. Like so:

/* The factory class provides each injected dependency */
createPlayerService(serviceType: string): PlayerService {
    switch(serviceType) {
        case 'offline':
            return new OfflinePlayerService(
                   // redundant
                   this.gameInventoryService,
                   this.gameWorldService
            );
        case 'online':
            return new OnlinePlayerService(
                   // redundant
                   this.gameInventoryService,
                   this.gameWorldService
            );
    }
}

You would have to feed each of the services' dependencies. Imagine if you had more dependencies needed by each player service. More code redundancies would come on your way! This is not clean and surely, it is not acceptable.

Solution

Consider the following approach using a proxy object solely for dependency injection. It is named as dependencies in the example below:

export class PlayerService {
    protected gameInventoryService: GameInventoryService;
    protected gameWorldService: GameWorldService;

    constructor(dependencies: PlayerServiceDependency) {
        const {
            gameInventoryService,
            gameWorldService,
        } = dependencies;

       this.gameInventoryService = gameInventoryService;
       this.gameWorldService = gameWorldService;
    }
}

There would be only one constructor parameter that contains all the dependencies of the player service. It is destructured so the class properties can be set. It even has its own type definition for more clarity, like so:

export type PlayerServiceDependency {
    gameInventoryService: GameInventoryService;
    gameWorldService: GameWorldService;
}

Now, going back to the factory example. You can leverage the proxy object to lessen the redundancies. Define the proxy object once and you are good to go. Like so:

/* The factory class provides each injected dependency */
createPlayerService(serviceType: string): PlayerService {
    const dependencies: PlayerServiceDependency = {
        gameInventoryService: this.gameInventoryService,
        gameWorldService: this.gameWorldService,
    }

    switch (serviceType) {
        case 'offline':
            return new OfflinePlayerService(dependencies);
        case 'online':
            return new OnlinePlayerService(dependencies);
    }
}

With this, you can easily add more service dependencies while being less redundant. Yay!

Closing Thoughts

This pattern is inspired by the time when I was still using awilix as my dependency injection (DI) container for JavaScript. By default, it used the similar proxy object pattern for its DI method.

By using proxy object DI method, we are losing the parameter properties shorthand that is so convenient. Nevertheless, the tradeoffs would be less redundant code!

Nowadays, I am using TypeScript and I am enjoying the dependency injection built into NestJS. Still, it is nice to see some lessons from the past carry over today. Old techniques can definitely be reused and applied in new frameworks. ๐Ÿ˜Ž

ย