npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

tsdim

v0.0.15

Published

Simple dependency injection manager without use of constructor and without the need of factories

Downloads

46

Readme

Typescript Dependency Injector Manager

Very small dependency injection manager which doesn't use the common pattern of injecting the dependencies via constructor and which also does not require a factory when instantiating a class annotated with Service.

Installation

npm install --save tsdim

Usage

Simple, direct injection

import {Service, Autowired} from 'tsdim';

@Service()
class ServiceA {
}

@Service()
class ServiceB {
    @Autowired(ServiceA) public a: ServiceA;
}

// No need of factories, or anything else.
const b = new ServiceB();
console.log('b.a is instantiated', b.a);

Injection tokens

import {Service, Autowired} from 'tsdim';

@Service('service-a')
class ServiceA {
}

@Service()
class ServiceB {
    @Autowired('service-a') public a: ServiceA;
}

// No need of factories, or anything else.
const b = new ServiceB();
console.log('b.a is instantiated', b.a);

Changing a service implementation:

import {Service, Autowired} from 'tsdim';

@Service()
class ServiceA {
}

@Service()
class ServiceB {
    @Autowired(ServiceA) public a: ServiceA;
}

class ServiceC {
}

Injector.provide({provide: ServiceA, useClass: ServiceC});

// No need of factories, or anything else.
const b = new ServiceB();
console.log('b.a is of type ServiceC', b.a instanceof ServiceC); // true
console.log('b.a is not of type ServiceC', b.a instanceof ServiceA); // false

Use factories

import {Service, Autowired} from 'tsdim';

@Service()
class ServiceA {
    constructor(private _config: MyConfiguration){}
}

@Service()
class ServiceB {
    @Autowired(ServiceA) private _a: ServiceA;
}

function FactoryA(config: MyConfiguration) {
    return new ServiceA(config);
}

Injector.provide({provide: ServiceA, useFactory: FactoryA, dependencies: [{...config object...}]});

// No need of factories, or anything else.
const b = new ServiceB();
console.log('b.a._config is instantiated and of type MyConfiguration', b.a['_config'] instanceof MyConfiguration); // true

If you want to see the rational behind it, then read on.

Problems with DI using constructors

Injecting dependencies via constructor is very bad for a few reasons:

  • We need to know the private dependencies of a service if we need to extend it.

  • In case we need to instantiate a class provided by a dependency injector container via the constructor, we always need a factory. This is why Angular is using the ComponentFactoryResolver service. Because in Angular components need to be instantiated and destroyed per request (they are not singleton) and a Factory is needed to get an instance of a given component.

And I know that we are being told that we should not instantiate classes with new or that we try to avoid extending classes. But there are lots of valid use cases to instantiate classes manually (see the mentioned case of Angular components) or to extend services or other things otherwise provided by a dependency injection container.

I've seen lots of code going to some out of the way trying to avoid extending something because then they would need to provide the private internal dependencies of the service they were trying to extend. I've also seen other code simply giving up and just aquiring the required service and passing it to the super class.

Examples of bad practice

Let's say we have a service playing videos. This service can use several backends to play videos (for example vlc and mplayer). This service is also using another service to get the metadata of the videos.

And then, depending on some factors (like user input, part in the application where we are rendering the player etc.) one of those backends will be used. This is a classical case of declaring an abstract class and then defining it afterwards. How is this model normally implemented, avoiding extension?

@Service()
export class MetadataService {
    public retrieveMetadata(videoId: string): Metadata {
        // ... retrieve the metadata
    }
}

export interface PlayerBackend {
    play(url: string);
}

@Service()
export class VlcBackend implements PlayerBackend {
    public play(url: string){
        // ... play video
    }
}

@Service()
export class MPlayerBackend implements PlayerBackend {
    public play(url: string){
        // ... play video
    }
}

export type BackendType = 'vlc' | 'mplayer';

@Service()
export class VideoService {
    constructor(
        private _metadataService: MetadataService,
        private _vlcBackend: VlcBackend;
        private _mplayerBackend: MPlaterBackend;
    ){}

    public play(videoId: string, byWhat: BackendType) {
        const metadata = this._metadataService.retrieveMetadata(videoId);

        if (byWhat == 'vlc') {
            this._vlcBackend.play(metadata.url);
            return ;
        }

        if (byWhat == 'mplayer') {
            this._mplayerBackend.play(metadata.url);
            return ;
        }

        throw new Error('BACKEND_NOT_IMPLEMENTED');
    }
}

@Service()
export class Consumer {
    constructor(private _videoService: VideoService){}

    public userChooseBackend(): BackendType {
        // ... return the user preferred backend
    }

    public playVideo(videoId: string) {
        this._videoService.play(videoId, this.userChooseBackend());
    }
}

Notice that if I would know from the beginning of the application that only one backend is choosen, then this is another scenario, and in this case I can just inject that specific backend based on some kind of token, and the implementation would be much simpler. But in the case I need to choose the backend at run time, depending on user preferences, then we have to go to a lot of overhead, just to avoid inheritance. Why we need that? Because, if we would use inheritance, it would look something like this:

export abstract class AbstractVideoService {
    constructor(
        @Inject() private _metadataService: MetadataService,
    ){}

    public abstract play(videoId: string);
}

@Service()
export class VlcBackend extends AbstractVideoService {
    constructor(private _metadataService: MetadataService) {
        super(this._metadataService);
        // ... initialization code here
    }

    public play(videoId: string){
        // ... play the video
    }
}

@Service()
export class MPlayerBackend extends AbstractVideoService {
    constructor(private _metadataService: MetadataService) {
        super(this._metadataService);
        // ... initialization code here
    }

    public play(videoId: string){
        // ... play the video
    }
}

@Service()
export class Consumer {
    private _backends: Map<string, AbstractVideoService> = new Map<string, AbstractVideoService>();
    constructor(private _vlcBackend: VlcBackend, private _mplayerBackend: MPlayerBackend){
        this._backends.set('vlc', this._vlcBackend);
        this._mplayerBackend.set('mplayer', this._mplayerBackend);
    }

    public userChooseBackend(): Backend {
        // ... return the user preferred backend
    }

    public playVideo(videoId: string) {
        this._backends.get(this.userChooseBackend()).play(videoId);
    }
}

Notice how in the pure object oriented paradigm, the responsibility of choosing the backend is completely the concern of the consummer? In the "do not use inheritance for the sake of not using inheritance" paradigm, this concern is shared between the service and the consumer.

But the issue with the inheritance paradigm is that the backend service needs to know that the video service internally has a dependency on the MetadataService. And of course, it could be 10 dependencies. The problem is the same. Also, imagine that you need now to extend the Consumer class. And on top of this, you have 10 possible backends. How would that look?

Of course, this can be modeled in other ways, but this is the main issue of passing injection tokens in constructor.

How else to do dependency injection?

What about the old way Spring java framework used to do it? Using the Service and Autowired paradigms.

So, let's consider the above mentioned example. In the inheritance paradigm, what if we have this:

export abstract class AbstractVideoService {
    @Autowired(MetadataService) private _metadataService: MetadataService;
    constructor(){}

    public abstract play(videoId: string);
}

@Service()
export class VlcBackend extends AbstractVideoService {
    constructor() {
        super();
        // ... initialization code here
    }

    public play(videoId: string){
        // ... play the video
    }
}

@Service()
export class MPlayerBackend extends AbstractVideoService {
    constructor() {
        super();
        // ... initialization code here
    }

    public play(videoId: string){
        // ... play the video
    }
}

@Service()
export class Consumer {
    private _backends: Map<string, AbstractVideoService> = new Map<string, AbstractVideoService>();
    @Autowired() private _vlcBackend: VlcBackend;
    @Autowired() private _mplayerBackend: MPlayerBackend

    constructor(){
        this._backends.set('vlc', this._vlcBackend);
        this._mplayerBackend.set('mplayer', this._mplayerBackend);
    }

    public userChooseBackend(): Backend {
        // ... return the user preferred backend
    }

    public playVideo(videoId: string) {
        this._backends.get(this.userChooseBackend()).play(videoId);
    }
}

In this example, now the backend can safely inherit the AbstractVideoService without worrying about it's internal dependencies.

This is what this dependency injection manager proposes: these 2 annotations (Autowired and Service).

For examples, see the first part of this read me.

The only downside

The only downside of doing DI this way, is that we have no way of knowing in a constructor if a service has been initialized or not, unless specified by the provider in the deps. So, if we want to use another service in the constructor that is also wired, we have to use the @PostConstruct annotation, to be sure that all services have been initialized.

@Service()
export class ServiceA {
    ...
}

@Service()
export class ServiceB {
    @Autowired(ServiceA) private _a: ServiceA;

    constructor() {
        // Here, this._a is probably undefined, so doing this._a.doStuff()
        // will result in an error.
    }

    @PostConstruct()
    private _init() {
        // Now, you are sure A is initialized, and you can use it
        this._a.doStuff();
    }
}