Inheritance and dependency injection
Asked Answered
S

8

139

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?

Surfeit answered 19/8, 2016 at 12:11 Comment(4)
This is not a duplicate. The question being referenced is about how to construct a DERIVED class that can acess a service injected by a already defined super class. My question is about how to construct a SUPER class that inherits a service to derived classes. It's simply the other way around.Surfeit
Your answer (inline in your question) doesn't make sense to me. This way you create an injector that is independent of the injector Angular uses for your application. Using new MyService() instead of injecting gives you exactly the same result (except more efficient). If you want to share the same service instance across different services and/or components, this will not work. Every class will get another MyService instance.Leith
You are completely right, my code will generate lots of instances of myService. Found a solution that avoids this but adds more code to the derived classes...Surfeit
Injecting the injector is only an improvement when there are several different services that need to be injected in many places. You can also inject a service that has dependencies to other services and provide them using a getter (or method). This way you only need to inject one service but can use a set of services. Your solution and my proposed alternative have both the disadvantage that they make it harder to see what class depends on what service. I'd rather create tools (like live templates in WebStorm) that make it easier to create the boilerplate code and be explicit about dependenciesLeith
S
83

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

Surfeit answered 19/8, 2016 at 14:47 Comment(7)
This makes it pretty hard to understand what services are actually injected though.Squabble
Shouldn't it be this.myServiceA = injector.get(MyServiceA); etc?Ignace
@Gunter Zochbauer's answer is the correct one. This is not the correct way to do this and breaks a lot of angular conventions. It might be simpler in that coding all those injection calls is a "pain", but if you want to sacrifice having to write constructor code for being able to maintain a large codebase, then you're shooting yourself in the foot. This solution isn't scalable, IMO, and will cause a lot of confusing bugs down the road.Washstand
There isn't a risk of multiple instances of the same service. You simply have to provide a service at the root of your application to prevent multiple instances that could occur on different branches of the application. Passing services down the inheritance change does not create new instances of the classes. @Gunter Zochbauer's answer is correct.Piave
@Surfeit did you ever explore extending Injector globally to avoid having to chain any parameters to AbstractComponent? fwiw, I think property injecting dependencies into a widely used base class to avoid messy constructor chaining is a perfectly valid exception to the usual rule.Lebkuchen
@Piave glad someone said that. I could not figure out why that ended up being agreed upon in the discussion!Ephraim
This injector passing hack causes very serious performance issues. Trust me, I've just experienced it. My recommendation is that everyone should avoid this completely.Fiske
S
94

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.

Schinica answered 19/8, 2016 at 12:13 Comment(24)
If I have to pass the needed service from every derived class to the super class then it's pointless to try to inject it in super class. Simply inject it within each derived class. Less code, better code readability.Surfeit
Sure, just inject it everywhere where you actually need it. If your superclass has some implementation that depends on it, then add it to the constructor so subclasses are required to pass it, otherwise just don't.Leith
Just to make it clear: I need it EVERYWHERE. Trying to move that dependency to my super class so that EACH derived class can access the service without the need to inject it individually to each derived class.Surfeit
You just need to add it as dependency everywhere where you need to access this service. If you need it in the superclass, then you also need to add it to each subclass because the constructor of the subclass is the only way to get it injected in the first place.Leith
That's obviously not true (any more), see my edit of the original question.Surfeit
I think it is. See my comment below your question.Leith
The answer to his own question is an ugly hack. The question already demonstrates how it should be done. I elaborated a bit more.Leith
This answer is correct. The OP answered their own question but broke a lot of conventions in doing so. That you listed the actual disadvantages is helpful as well and I will vouch for it - I was thinking the same thing.Washstand
I really want to (and continue to) use this answer over the OP's "hack". But I have to say that this seems far from DRY and is very painful when I want to add a dependency in the base class. I just had to add ctor injections (and the corresponding super calls) to about 20+ classes and that number is only going to grow in the future. So two things: 1) I'd hate to see a "large codebase" do this; and 2) Thank God for vim q and vscode ctrl+.Exsect
@ibgib it's the same in all languages I know. Constructors are difficult if you want to make guarantees about behavior.Leith
I have the same issue: I have lots of components that all depend on the same initial global registration based on a configuration, and it seemed elegant to put that code in a base class and let all components inherit that behavior. The registration and configuration uses several singleton services, some of which are only necessary at initiation. However, this issue with dependency injection makes sub-classing messier than the alternative--creating a utility service to inject in each component. It is worth considering whether inheritance is really what you want.Ehrsam
I don't understand why people are so upset that they have to pass in the injected service(s). This answer is correct, even if the others are cute - this is just as it is in other languages.Piave
@Piave I see it the same way. I got the impression it depends where you come from. If you come fom Java or similar, it's how the world works, if you come from JS, even imports are considered an insane amount of boilerplate and constructors are an attempt of the Illuminaty to drive every developer insane ;-)Leith
Can you @Autowire properties in spring, in a super class, without having to pass them as constructor arguments in the sub class? If so, I would see this as a framework limitation, and not necessarily how every other language (or framework) works. It would also kind of discredit the "hides dependencies" statement, unless that's just a bad practice in general for spring developers. Disclaimer: I'm no java/spring expert and not concerned with the Illuminati.Bobsled
I don't know what you mean with spring. Angular DI only works with constructor parameters, not with properties. Hiding dependencies is not the intention. I wouldn't see this as a benefit anyway.Leith
This is not good coding standard. Makes code harder to read? What are you talking about, if you are using a inherited class, you should have looked at that class before inheriting it. It's wasteful and a mess. No low level development practices follow this methodology.Snot
@Snot For "your answer is completely wrong" I'd expect more concrete information about why my answer is wrong. Your comment sounds like an overall rant. Sure, you can try to design the classes differently, but this kind of question was asked many times and was usually about "how can I work around the requirement to specify all parameters in the subclass again". After I posted this answer Angular started to support constructors in the superclass only if no additional parameters were required for the subclass.Leith
@GünterZöchbauer for one, services initialization from the context required and not injection through Injectable/DI is always preferred. Services should be self dependent. Meaning that a super class can have it's reference to something that isn't chained down from reference and context to higher level classes that are extending it for the purpose of using those methods through inheritance. Also for example in java you can bind beans in the base class and inherit the properties of that class without passing dependency chains through a constructor. Avoiding a ridiculous amount of mess.Snot
@GünterZöchbauer secondly this is not the type of pattern that should be promoted. This is why resource factories exist. Why do you think react does not follow this pattern......... It's not a rant, it's best practice and of high level development, an industry standard to not bloat code, this is also why functional programming is becoming more popular.......Snot
Angular only supports constructor injection. Your comment is not really related to the question or my answer. It's a question about a technical problem and I answered the question. Your comment is about architecture decisions. Thanks for your more elaborate statement. It's still just a list of statements without arguments why your statements are true. I don't think this is the right place for this kind of discussion.Leith
@GünterZöchbauer I am more so criticizing angular yes, heavily annoyed with a project I had been working on with it, currently porting it to react for reasons stated above among others. But your comment seemed to be promoting this as an architecture bonus, when IMO I would say it largely is not.Snot
I can not agree with this answer, because even if this is the good practice from Angular. This is not a good way in programming, just take this example: I have a base class injecting 15 services and is inherited by 6 child classes, DI list is not readable, and when I have to add one service or one new child, it comes to be very complicated for only one DI/child.Cavorilievo
Just because it is inconvenient does not mean it is bad practice. Constructors are inconvenient because it is very difficult to get object initialization done reliably. I'd argue the worse practice is building a service that needs "a base class injecting 15 services and is inherited by 6".Leith
@Cavorilievo please reconsider to split up a base class with 15! Injected services. That must be a design flaw.Hypoxia
S
83

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

Surfeit answered 19/8, 2016 at 14:47 Comment(7)
This makes it pretty hard to understand what services are actually injected though.Squabble
Shouldn't it be this.myServiceA = injector.get(MyServiceA); etc?Ignace
@Gunter Zochbauer's answer is the correct one. This is not the correct way to do this and breaks a lot of angular conventions. It might be simpler in that coding all those injection calls is a "pain", but if you want to sacrifice having to write constructor code for being able to maintain a large codebase, then you're shooting yourself in the foot. This solution isn't scalable, IMO, and will cause a lot of confusing bugs down the road.Washstand
There isn't a risk of multiple instances of the same service. You simply have to provide a service at the root of your application to prevent multiple instances that could occur on different branches of the application. Passing services down the inheritance change does not create new instances of the classes. @Gunter Zochbauer's answer is correct.Piave
@Surfeit did you ever explore extending Injector globally to avoid having to chain any parameters to AbstractComponent? fwiw, I think property injecting dependencies into a widely used base class to avoid messy constructor chaining is a perfectly valid exception to the usual rule.Lebkuchen
@Piave glad someone said that. I could not figure out why that ended up being agreed upon in the discussion!Ephraim
This injector passing hack causes very serious performance issues. Trust me, I've just experienced it. My recommendation is that everyone should avoid this completely.Fiske
S
7

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) {
    }
}
Sailer answered 17/8, 2017 at 22:38 Comment(1)
The problem here is that you risk creating a "junk drawer" service that's essentially just a proxy for the Injector service.Hostler
S
7

As far as I can tell this is now possible in a very simple way with Angular v14 using inject() https://angular.io/api/core/inject, because you can now set a dependency in a "field initializer", outside the constructor entirely.

I used some simple token dependencies, DOCUMENT and LOCALE_ID as examples.

import { DOCUMENT } from '@angular/common';
import { inject } from '@angular/core';

export abstract class AbstractComponent {
  abstractDependency = inject(DOCUMENT);
}
import { Component, inject, LOCALE_ID } from '@angular/core';
import { AbstractComponent } from './abstract.component';

@Component({
  selector: 'my-app',
  template: '{{ abstractDependency }} {{ myDependency }}',
})
export class MyComponent extends AbstractComponent {
  myDependency = inject(LOCALE_ID);
}

Live Example: https://stackblitz.com/edit/angular-ivy-zvlx1d?file=src/app/my.component.ts

Scrabble answered 21/10, 2022 at 4:44 Comment(2)
exacly it! I was wondering if I can inject omitting constructor :)Orthman
This works from v14 onwards !Dhyana
A
2

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.

Acoustician answered 20/2, 2018 at 22:16 Comment(1)
The base class then has dependencies on all services any of its children needs. ChildComponentA needs ServiceA? Well now ChildComponentB gets ServiceA too.Juna
F
1

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.

Fistulous answered 21/2, 2019 at 21:27 Comment(0)
O
1

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);
  }
}
Outlaw answered 26/6, 2020 at 11:20 Comment(0)
B
-1

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);
  }
}
Bircher answered 12/10, 2017 at 3:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.