Inheritance and dependency injection

47,971

Solution 1

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 2

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 3

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

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

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.

Share:
47,971
maxhb
Author by

maxhb

Code like a boyscout: Always leave the campground cleaner than you found it. If you mix one kilogramm of cowberry with one kilogramm of shit, you'll get 2 kilogramms of shit [Boris Nemzow]

Updated on June 27, 2020

Comments

  • maxhb
    maxhb about 4 years

    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?

  • maxhb
    maxhb almost 8 years
    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.
  • Günter Zöchbauer
    Günter Zöchbauer almost 8 years
    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.
  • maxhb
    maxhb almost 8 years
    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.
  • Günter Zöchbauer
    Günter Zöchbauer almost 8 years
    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.
  • maxhb
    maxhb almost 8 years
    That's obviously not true (any more), see my edit of the original question.
  • Günter Zöchbauer
    Günter Zöchbauer almost 8 years
    I think it is. See my comment below your question.
  • Günter Zöchbauer
    Günter Zöchbauer almost 8 years
    The answer to his own question is an ugly hack. The question already demonstrates how it should be done. I elaborated a bit more.
  • Simon Dufour
    Simon Dufour over 7 years
    This makes it pretty hard to understand what services are actually injected though.
  • jenson-button-event
    jenson-button-event about 7 years
    Shouldn't it be this.myServiceA = injector.get(MyServiceA); etc?
  • dudewad
    dudewad about 7 years
    @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.
  • dudewad
    dudewad about 7 years
    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.
  • Admin
    Admin almost 7 years
    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+.
  • Günter Zöchbauer
    Günter Zöchbauer almost 7 years
    @ibgib it's the same in all languages I know. Constructors are difficult if you want to make guarantees about behavior.
  • ScottG
    ScottG almost 7 years
    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.
  • ktamlyn
    ktamlyn almost 7 years
    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.
  • ktamlyn
    ktamlyn almost 7 years
    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.
  • Günter Zöchbauer
    Günter Zöchbauer almost 7 years
    @ktamlyn 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 ;-)
  • kpup
    kpup over 6 years
    The problem here is that you risk creating a "junk drawer" service that's essentially just a proxy for the Injector service.
  • Brandon G
    Brandon G over 6 years
    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.
  • Günter Zöchbauer
    Günter Zöchbauer over 6 years
    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.
  • thekevshow
    thekevshow about 6 years
    This is not good coding standard your answer is completely wrong. 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. This is why angular is trash.
  • Günter Zöchbauer
    Günter Zöchbauer about 6 years
    @thekevshow 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.
  • thekevshow
    thekevshow about 6 years
    @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.
  • thekevshow
    thekevshow about 6 years
    @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.......
  • Günter Zöchbauer
    Günter Zöchbauer about 6 years
    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.
  • thekevshow
    thekevshow about 6 years
    @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.
  • Loenix
    Loenix almost 6 years
    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.
  • Günter Zöchbauer
    Günter Zöchbauer almost 6 years
    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".
  • quentin-starin
    quentin-starin almost 6 years
    @maxhb 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.
  • aruno
    aruno over 5 years
    @ktamlyn glad someone said that. I could not figure out why that ended up being agreed upon in the discussion!
  • knallfrosch
    knallfrosch about 5 years
    The base class then has dependencies on all services any of its children needs. ChildComponentA needs ServiceA? Well now ChildComponentB gets ServiceA too.
  • webpreneur
    webpreneur over 3 years
    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.
  • Bernoulli IT
    Bernoulli IT over 3 years
    @Loenix please reconsider to split up a base class with 15! Injected services. That must be a design flaw.