AngularJS: Do inherited dependencies need to be duplicated?

Using Angular 1.6 in combination with ES6 classes, I ran into the following problem:

I wrote a service with some dependencies (surprise!)

class myService {

    /*@ngInject*/
    constructor($q){
      this.$q = $q;
      this.creationDate = new Date();
    }

    doStuff(data){
      return this.$q.when(data);
    }
}

angular.module('app').service('myService', myService)

      

However, I have a build target where the service should be a little more interesting, so I extended it and used the extended service instead:

class myFancyService extends myService{

    /*@ngInject*/
    constructor($q, $http){
      super($q);
      this.$http = $http;
    }

    doFancyStuff(data){
      console.log(this.creationDate);
      return this.doStuff(data)
          .then((stuff) => this.$http.post('api.myapp', stuff));
    }
}

angular.module('app').service('myService', myFancyService)

      

This works great so far, but has a big drawback:

By calling super(dependencies)

, the dependencies of my base class cannot be auto-injected from @ngInject

. Thus, I need to know that anytime I change the dependencies myService

, the dependencies myFancyService

(and any other potential future child class) also need to be changed .

I cannot use Composition instead of Inheritance because I am not myService

registered as an angular service and therefore cannot be injected as a dependency.

Question:

Is there a way to automatically inject base class dependencies?

If not, is there at least a way for my unittests to remind me that I need to update the dependencies myFancyService

? I couldn't find a way to check with karma / jasmine yet if the arguments (or maybe just the number of arguments) super($q)

are equal to (the number) of myService- arguments constructor

.

+3


source to share


2 answers


There are two things to keep in mind:

  • in Inheritance

    A pattern having interface consistency is essential, child classes can reimplement methods or properties, but they cannot change the way the method is called (arguments, etc.)
  • You are still registeringBaseService

    with dependency injection

    , but you may not need to do this because it looks like an abstract class to you.

This might solve your problem (run the script to see what's going on) You basically need to extend static $inject property

in each derived class

and use destructuring in each child constructor:

  • Benefits . You don't need to know what dependencies the parent class has.
  • Constraints : always use the first parameters in your child class (because it rest operator

    should be the last one)


function logger(LogService) {
  LogService.log('Hello World');
}

class BaseService {
  static get $inject() { 
    return ['$q']; 
  }

  constructor($q) {
    this.$q = $q;
  }
  
  log() {
    console.log('BaseService.$q: ', typeof this.$q, this.$q.name);
  }
}

class ExtendedService extends BaseService {
  static get $inject() { 
    return ['$http'].concat(BaseService.$inject); 
  }

  constructor($http, ...rest) {
    super(...rest);
    this.$http = $http;
  }
  
  log() {
    super.log();
    console.log('ExtendedService.$http: ', typeof this.$http, this.$http.name);
  }
}


class LogService extends ExtendedService {
  static get $inject() { 
    return ['$log', '$timeout'].concat(ExtendedService.$inject); 
  }

  constructor($log, $timeout, ...rest) {
    super(...rest);
    this.$log = $log;
    this.$timeout = $timeout;
  }
  
  log(what) {
    super.log();
    this.$timeout(() => {
      this.$log.log('LogService.log', what);
    }, 1000);
  }
}

angular
  .module('test', [])
  .service('BaseService', BaseService)
  .service('ExtendedService', ExtendedService)
  .service('LogService', LogService)
  .run(logger)
;
      

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.js"></script>

<section ng-app="test"></section>
      

Run codeHide result



I also opened feature request

in babel-plugin-angularjs-annotate: https://github.com/schmod/babel-plugin-angularjs-annotate/issues/28

+2


source


The code above super

requires the arguments to be explicitly specified.

A more fail-safe way is to do all of the dependency assignments in the current class:

constructor($q, $http){
  super();
  this.$q = $q;
  this.$http = $http;
}

      

This can create problems if these services are used in the parent constructor. It is not easy to check the arguments of the parent constructor because it has to do with the mocks module. A simple and relatively reliable way to test this is to assert:

expect(service.$q).toBe($q);
expect(service.$http).toBe($http);

      

This should be done in any Angular unit test, even if the class was not inherited.

Your best bet is to introduce a base class that handles DI, assuming that whatever it @ngInject

does is create an annotation $inject

:



class Base {
  constructor(...deps) {
    this.constructor.$inject.forEach((dep, i) => {
      this[dep] = deps[i];
    }
  }
}
BaseService.$inject = [];

class myService extends Base {
    /*@ngInject*/
    constructor($q){
      super(...arguments);
      ...
    }
    ...
}

      

At this point, it becomes apparent that it is @ngInject

no longer helping and he needs to mess with arguments

. Without @ngInject

it, it becomes:

class myService extends Base {
    static get $inject() {
      return ['$q'];
    }

    constructor(...deps){
      super(...deps);
      ...
    }
    ...
}

      

If the assignment of the dependencies are the only things that are done in the child constructor, the constructor can be effectively omitted:

class myService extends Base {
    static get $inject() {
      return ['$q'];
    }

    ...
}

      

It even beats class fields and Babel / TypeScript (no native browser support):

class myService extends Base {
    static $inject = ['$q'];

    ...
}

      

0


source







All Articles