Willem van Gerven
Willem van Gerven

Reputation: 1587

Dependency injection: What I am taught and why I don't understand that

I am currently learning myself Angular 2. I found this video which I found particularly helpful in this: https://www.youtube.com/watch?v=_-CD_5YhJTA

However, with regards to dependency injection, a topic Mosh starts on 36:40, I do not seem to fully understand his arguments.

Namely, he states (at 41:50) that by writing the constructor like this

export class CoursesComponent {
    title = "The title of courses page";
    courses;

    constructor (courseService: CourseService) { 
        this.courses = courseService.getCourses();
    }
}

"we wrote a component that is no longer tightly coupled to the service" (quote).

Now I tried to convince myself that it is, but I cannot see how it is. Indeed, we could create a mock service CourseService for other usages of this class. However, there is explicit reference in the constructor to a method which is defined in the service CourseService. In my mind, this makes it clearly not a reusable (and testable) class in this way.

Wouldn't it make much more sense to write something like

constructor (courseService) { 
    this.courses = courseService;
}

and then to let us worry about what we pass to the constructor somewhere else. Then, in my mind, we are honestly decoupled from the service.

But am I right? Or am I missing the point here? And also, as I am a novice, but in case that I have a point, perhaps somebody could propose and alternative for writing this class along the lines of my concerns?

Thanks a lot.

Willem

Upvotes: 0

Views: 215

Answers (4)

Günter Zöchbauer
Günter Zöchbauer

Reputation: 657298

In addition to @Piccis answer.

Hollywood principle (loose coupling)

The main thing here is similar to the Hollywood principle. "Don't call us, we call you"

If you would use new Xxx() in your class it would be very tight coupling:

export class CoursesComponent {
    title = "The title of courses page";
    courses;

    constructor () { 
        let courseService = new CourseService();
        this.courses = courseService.getCourses();
    }
}

but with the original code from your question any class that extends CourseService can be passed in and it is also possible to define how this injected instance should be initialized (see next code snipped below).

any class that extends CourseService is because of a TypeScript limitation where interfaces are not available at runtime.

In Dart for example it would be any class that implements CourseService

configure a dependency

class CourseServiceConfiguration {
  ...
}

class CourseService {
  constructor(private config:CouseServiceConfiguration) {}
  getCourses() { ... }
}

bootstrap(AppComponent, [
  CourseServiceConfiguration,
  CourseService]);

Would provide an instance of CourseService configured with an instance of CourseServiceConfiguration.

Use other key types for DI

If this is still too tight you can use other keys like string or OpaqueToken.

bootstrap(AppComponent, [provide('courseService', {useClass: CourseService})]);

this way you can inject it like

export class CoursesComponent {
    title = "The title of courses page";
    courses;

    constructor (@Inject('courseService') courseService) { 
        this.courses = courseService.getCourses();
    }
}

but this way you loose type checking and autocompletion support because courseService is not types. This way would allow you to use interfaces in TypeScript as well

export interface CourseService {
  getCourses();
}

export class CoursesComponent {
    title = "The title of courses page";
    courses;

    constructor (@Inject('courseService') courseService:CourseService) { 
        this.courses = courseService.getCourses();
    }
}

allows you to inject any type that implements CourseService and still get static type checking and proper autocompletion.

OpaqueToken is similar to a string key but it prevents two keys with colliding strings would still be distinguishable. In OpaqueToken the string is only informal.

Upvotes: 1

Shane Edwards
Shane Edwards

Reputation: 46

I'm fairly new to Angular 2 but my understanding is that Angular 2 has hierarchical injectors, I can certainly see the constructor typing being useful in telling Angular to find the nearest ancestor instance of the injectable to use. There could be benfits in using sub-classes of courseService so you can have different behaviour depending on the component context.

More on hierarchical injectors: https://angular.io/docs/ts/latest/guide/hierarchical-dependency-injection.html

Upvotes: 1

Picci
Picci

Reputation: 17762

I think the secret is that you can inject a different implementation of CourseService from your main.ts (or whatever file defines your initial bootstrap).

This is an example that could work

import {provide}    from 'angular2/core';

import {bootstrap}    from 'angular2/platform/browser';

import {AppComponent} from './app.component';

import {CourseService} from './course.service';
import {MyCourseService} from '../myImplementation/myCourse.service';

bootstrap(AppComponent, 
            [provide(CourseService, {useClass: MyCourseService})]);

I hope this helps

Upvotes: 2

kemsky
kemsky

Reputation: 15271

Coupling would assume excessive dependency on Service. I guess it is not the case here since Service has clean interface - getCourses(). We could make it even less coupled injecting interface with opaque token like IService{getCourses()} and it would make the code complicated and less convinient to use, but...

JavaScript allows us to mock or provide different implementations at runtime without casting errors(duck typing). Check this article.

Upvotes: 1

Related Questions