Inheritance and dependency injection

AngularInheritanceInjecttypescript1.8

Angular Problem Overview


I have a set of angular2 components that should all get some service injected. My first thought was that it would be best to create a super class and inject the service there. Any of my components would then extend that superclass but this approach does not work.

Simplified example:

export class AbstractComponent {
  constructor(private myservice: MyService) {
    // Inject the service I need for all components
  }
}

export MyComponent extends AbstractComponent {
  constructor(private anotherService: AnotherService) {
    super(); // This gives an error as super constructor needs an argument
  }
}

I could solve this by injecting MyService within each and every component and use that argument for the super() call but that's definetly some kind of absurd.

How to organize my components correctly so that they inherit a service from the super class?

Angular Solutions


Solution 1 - Angular

> I could solve this by injecting MyService within each and every component and use that argument for the super() call but that's definetly some kind of absurd.

It's not absurd. This is how constructors and constructor injection works.

Every injectable class has to declare the dependencies as constructor parameters and if the superclass also has dependencies these need to be listed in the subclass' constructor as well and passed along to the superclass with the super(dep1, dep2) call.

Passing around an injector and acquiring dependencies imperatively has serious disadvantages.

It hides dependencies which makes code harder to read.
It violates expectations of one familiar with how Angular2 DI works.
It breaks offline compilation that generates static code to replace declarative and imperative DI to improve performance and reduce code size.

Solution 2 - Angular

Updated solution, prevents multiple instances of myService being generated by using the global injector.

import {Injector} from '@angular/core';
import {MyServiceA} from './myServiceA';
import {MyServiceB} from './myServiceB';
import {MyServiceC} from './myServiceC';

export class AbstractComponent {
  protected myServiceA:MyServiceA;
  protected myServiceB:MyServiceB;
  protected myServiceC:MyServiceC;

  constructor(injector: Injector) {
    this.settingsServiceA = injector.get(MyServiceA);
    this.settingsServiceB = injector.get(MyServiceB);
    this.settingsServiceB = injector.get(MyServiceC);
  }
}

export MyComponent extends AbstractComponent {
  constructor(
    private anotherService: AnotherService,
    injector: Injector
  ) {
    super(injector);

    this.myServiceA.JustCallSomeMethod();
    this.myServiceB.JustCallAnotherMethod();
    this.myServiceC.JustOneMoreMethod();
  }
}

This will ensure that MyService can be used within any class that extends AbstractComponent without the need to inject MyService in every derived class.

There are some cons to this solution (see Ccomment from @Günter Zöchbauer below my original question):

  • Injecting the global injector is only an improvement when there are several different services that need to be injected in many places. If you just have one shared service then it's probably better/easier to inject that service within the derived class(es)
  • My solution and his proposed alternative have both the disadvantage that they make it harder to see which class depends on what service.

For a very well written explanation of dependency injection in Angular2 see this blog post which helped me greatly to solve the problem: http://blog.thoughtram.io/angular/2015/05/18/dependency-injection-in-angular-2.html

Solution 3 - Angular

Instead of injecting all the services manually I created a class providing the services, e.g., it gets the services injected. This class is then injected into the derived classes and passed on to the base class.

Derived class:

@Component({
	...
	providers: [ProviderService]
})
export class DerivedComponent extends BaseComponent {
	constructor(protected providerService: ProviderService) {
		super(providerService);
	}
}

Base class:

export class BaseComponent {
    constructor(protected providerService: ProviderService) {
		// do something with providerService
	}
}

Service-providing class:

@Injectable()
export class ProviderService {
    constructor(private _apiService: ApiService, private _authService: AuthService) {
    }
}

Solution 4 - Angular

Instead of injecting a service that has all the other services as dependencies, like so:

class ProviderService {
    constructor(private service1: Service1, private service2: Service2) {}
}

class BaseComponent {
    constructor(protected providerService: ProviderService) {}

    ngOnInit() {
        // Access to all application services with providerService
        this.providerService.service1
    }
}

class DerivedComponent extends BaseComponent {
    ngOnInit() {
        // Access to all application services with providerService
        this.providerService.service1
    }
}

I would skip this extra step and simply add inject all the services in the BaseComponent, like so:

class BaseComponent {
    constructor(protected service1: Service1, protected service2: Service2) {}
}

class DerivedComponent extends BaseComponent {
    ngOnInit() {
        this.service1;
        this.service2;
    }
}

This technique assumes 2 things:

  1. Your concern is entirely related to components inheritance. Most likely, the reason you landed on this question is because of the overwhelming amount of non-dry (WET?) code you need to repeat in each derived class. If you want to benefits of a single entry point for all your components and services, you will need to do the extra step.

  2. Every component extends the BaseComponent

There is also a disadvantage if you decide use the constructor of a derived class, as you will need to call super() and pass in all the dependencies. Although I don't really see a use case that necessitates the use of constructor instead of ngOnInit, it is entirely possible that such a use case exists.

Solution 5 - Angular

From what I understand in order to inherit from base class you first need to instantiate it. In order to instantiate it you need to pass its constructor required parameters thus you pass them from child to parent thru a super() call so it makes sense. Injector of course is another viable solution.

Solution 6 - Angular

UGLY HACK

Some time ago some of my client wants to join two BIG angular projects to yesterday (angular v4 into angular v8). Project v4 uses BaseView class for each component and it contains tr(key) method for translations (in v8 I use ng-translate). So to avoid switching translations system and edit hundreds of templates (in v4) or setup 2 translation system in parallel I use following ugly hack (I'm not proud of it) - in AppModule class I add following constructor:

export class AppModule { 
    constructor(private injector: Injector) {
        window['UglyHackInjector'] = this.injector;
    }
}

and now AbstractComponent you can use

export class AbstractComponent {
  private myservice: MyService = null;

  constructor() {
    this.myservice = window['UglyHackInjector'].get(MyService);
  }
}

Solution 7 - Angular

If parent class have been got from 3rd party plug-in (and you can't change the source) you can do this:

import { Injector } from '@angular/core';

export MyComponent extends AbstractComponent {
  constructor(
    protected injector: Injector,
    private anotherService: AnotherService
  ) {
    super(injector.get(MyService));
  }
}

or most better way (stay only one parameter in constructor):

import { Injector } from '@angular/core';

export MyComponent extends AbstractComponent {
  private anotherService: AnotherService;

  constructor(
    protected injector: Injector
  ) {
    super(injector.get(MyService));
    this.anotherService = injector.get(AnotherService);
  }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionmaxhbView Question on Stackoverflow
Solution 1 - AngularGünter ZöchbauerView Answer on Stackoverflow
Solution 2 - AngularmaxhbView Answer on Stackoverflow
Solution 3 - AngularLeukippView Answer on Stackoverflow
Solution 4 - AngularMaxime DupréView Answer on Stackoverflow
Solution 5 - AngularihorbondView Answer on Stackoverflow
Solution 6 - AngularKamil KiełczewskiView Answer on Stackoverflow
Solution 7 - AngulardlnskView Answer on Stackoverflow