CBAC #3a/Dependency Injection/

in utopian-io •  last year

What Will I Learn?

  • Dependency Injection as a pattern
  • Dependency Injection in Angular

Requirements

  • just previous parts of the course

Difficulty

  • intermediate

Dependency Injection

"Dependency Injection" is a 25-dollar term for a 5-cent concept.
~James Shore, source: jamesshore.com

image.png

You might have already heard the term Dependency Injection or briefly DI... and chances are you found it difficult. It turns out that DI as a pattern is really simple and its implementation in Angular doesn't harm developers.

Why?

Before I describe what DI is, let me tell you why do we need it. Look at this code:

class Rocket {
    private engine: Engine;
    private propellantTank: PropellantTank;
    constructor() {
        this.engine = new Engine();
        this.propellantTank= new PropellantTank();
    }
    startEngine() {
        this.engine.start();
        console.log('Engine has just started!');
    }
}

This class is bad and it doesn't use DI. The constructor knows exactly how to create all dependencies. That's bad.

First of all, it makes a class hard to test. This approach causes it almost impossible to mock out the dependencies when writing your unit test. All developers want to write easy to test code.

You can also imagine what would happen if you wanted to create an object using this class, but instead of Engine, you would want to use a SuperEngine version of the engine. As long as all dependencies are created inside the class, you lose the possibility to do so. As you can see, our class is not only untestable but also inflexible.

What can we do then? There is a solution - the class should ask for dependencies it needs instead of creating them inside.

And here comes DI as a pattern.

How?

Let's refactor the Rocket class:

class Rocket {
    private engine: Engine;
    private propellantTank: PropellantTank;
    constructor(engine, propellantTank) {
        this.engine = engine;
        this.propellantTank = propellantTank;
    }
    startEngine() {
        this.engine.start();
        console.log('Engine has just started!');
    }
}

Now we can create an object based on this class in order to test:

const testRocket = new Rocket(new MockEngine(), new MockPropellantTank());

or more powerful version of rocket using SuperEngine:

const falcon9 = new Rocket(new SuperEngine(), new PropellantTank());

As you can see, we told the constructor of this class that all it needs are three dependencies. However, our class doesn't know how they were created. We have moved the responsibility to create them to the outside world. And that's the pattern called Dependency Injection. Yeah, it's as simple as that!

DI as a framework

We are happy because our Rocket class is now awesome. But we haven't really thought of its consumers yet. What if Rocket would require more dependencies to work? For developers, it would be complicated to create all of them each time they wanted to use new Rocket. And here comes DI as a framework.

It turns out that it can solve all of our problems - the Rocket class won't care how to create dependencies it needs and the consumer won't care how to create the Rocket. How? Imagine this code was real:

const injector = new Injector();
let saturnV = injector.get(Rocket);
saturnV.startEngine();

It would be definitely awesome if we could do that!

Services in Angular

We have already introduced DI both as a pattern and framework. Now it's time to take a look how to use DI framework built in Angular to inject services.

Let a fragment of TaskService be our example (note that the entire code is available on Github, but the part which will contain explanation what is going on inside service's methods is gonna be in the next tutorial - 3b).

@Injectable()
export class TaskService {
  constructor(private afs: AngularFirestore, private afAuth: AngularFireAuth) {}
  getTask(taskId: string, userId: string) {
    return this.afs.doc<Task>(`users/${userId}/tasks/${taskId}`);
  }
}

As you can see, TaskService needs two dependencies, AngularFirestore and AngularFireAuth, but we moved the responsibility to deliver them to the external parts of the app.

But how does Angular know that some external dependencies are needed to be injected into the constructor of a class? The answer is simple: Angular doesn't know until we tell him. How can we force the framework to look at the constructor and deliver us the required dependencies?

It turns out that Angular injects dependencies declared in the class constructor only if the class has a decorator, for example @Injectable() or @Component(). To prove I'm not lying and every class which has a decorator can have some dependencies injected into its constructor, look at the following AppComponent fragment:

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  constructor(private taskService: TaskService, private authService: AuthService) {}
}

Ok, we now know that Angular somehow injects the dependencies from class constructor if the class has a decorator. But how does it know which instance of dependency should be injected? The answer is a bit tricky, and to understand what's going on I have to introduce providers.

Providers

I'm sure you have already heard about them. Nah, @jakipatryk, I have not. If you haven't, then go back to the 2nd part of the CBAC where I told you that @NgModule() takes some configuration options. One of them was an array of providers, which I described as a perfect place to put your services.

I wasn't lying, providers array is really a perfect place to put some services. The interesting fact is that not only modules have such an option. We could also add services directly to the @Component():

@Component({
  selector: 'app-root',
  providers: [TaskService, AuthService],
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  constructor(private taskService: TaskService, private authService: AuthService) {}
}

What is the difference? The place where you register a service is crucial.
In case you did it in @Component(), your service will have limited lifetime and scope. Its instance will only exist within the lifetime of a component. If you decide to put your service into @NgModule(), the instance of this service will exist within the lifetime of the app.

But what are providers? One of the definitions is that provider describes how the Injector should be configured. To understand it better you have to realize that the providers array you saw earlier is just a shorthand syntax:

providers: [TaskService, AuthService]

is actually the same as this:

providers: [{ provide: TaskService, useClass: TaskService }, { provide: AuthService, useClass: AuthService }

Now it is more clear that injector whenever needs TaskService or AuthService have a point to go to check what it exactly needs to use.

Also, services are singletons only within the scope of the injector. It implies that the scope of service depends on the place where it has been registered. I haven't mentioned yet that there is one root injector that is being created during the bootstrap of the app. However, there are also being created injectors for each instance of, for example a component that has providers in its configuration options.

Summary

In this tutorial I have described Dependency Injection as a pattern and then as a framework. I have also touched on the Angular's own DI system and services. As always, more on these topics can be found in the references section.

References:

DI pattern:

DI in Angular:


Curriculum

Do you like Science, Technology, Engineering or Mathematics? Check out #steemSTEM!



Posted on Utopian.io - Rewarding Open Source Contributors

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

Thank you for the contribution. It has been approved.

  • Please remove the banner at the bottom and all the mentions in the post to avoid rejection.
    You can contact us on Discord.
    [utopian-moderator]

Hey @jakipatryk I am @utopian-io. I have just upvoted you!

Achievements

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • You are generating more rewards than average for this category. Super!;)
  • Seems like you contribute quite often. AMAZING!

Suggestions

  • Contribute more often to get higher and higher rewards. I wish to see you often!
  • Work on your followers to increase the votes/rewards. I follow what humans do and my vote is mainly based on that. Good luck!

Get Noticed!

  • Did you know project owners can manually vote with their own voting power or by voting power delegated to their projects? Ask the project owner to review your contributions!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x