How to do dependency inversion in Angular


Wanna beef the quality of your Angular application?

Then read this to learn why using dependency inversion could help you dodge a kick in the teeth.

How about we begin with a story.

Did you hear about the colors.js library? That was smashed to bits n pieces by its own creator?

In case you've never heard of it, colors.js is a library created by an open source contributor named Marak. It was a cool library that allowed you to color your console.log() statements like this.

https://raw.githubusercontent.com/Marak/colors.js/master/screenshots/colors.png

It's a genuine NPM celebrity!

I just checked the stats and it's averaged around 20 - 25 million downloads per week!

😯 😯 😯

So what's the story?

In early 2022 the creator and maintainer decided to push a release that contained a well-crafted infinite loop that began at 666. This "bug" 🐞 was responsible for crashing thousands if not millions of projects and other NPM packages that depended on one small library.

And when folks discovered what he had done the open source community caught fire and with the help of NPM and Github they stole the project from Marak, the original creator.

But why did he do it? Why did burn years of hard work?

So far I haven't heard that he has yet publicly given the reason for purposefully adding an infinite loop to such a popular library. Why would you blow up your hard work anyway?

68747470733a2f2f6d656d652e6571382e65752f726d2e676966

He did fling some hints here and there on the web that he was fed up with billionaire companies using his free library.

What ever the case, we'll let Marak figure out what he's going to do with his life. The point of this story is to learn why we should consider wrapping dependencies.

Why?

Because if we had an Angular app that depended on colors.js, but we had wrapped the dependency, then dodging this kind of bullet would be easy.

But if we had imported colors.js directly into our app, without wrapping this dependency, we'd have probably taken the bullet smack on.

So, how do you wrap dependencies? In a way that makes your Angular app resistant to these kinds of wild-west scenes that rear their faces on the internet?

How to wrap a dependency?

It boils down to 3 simple steps.

  1. Create the interface.
  2. Add an implementation for our new interface.
  3. Use the injector to link interface to implementation.

This also known as dependency inversion. If you're not familiar with dependency inversion then the video below has one of the best explanations that I've ever watched.

We'll begin with the interface. Grab the Angular CLI and create a new project for this quick demo.

...

...

Ready?

...

I know it'd be easier to just keep reading but you'll learn the most by following along. So please, make sure you've got the Angular CLI ready.

Here's the first command.

ng new DependencyDemo

And once that command completes we'll need to cd into the new directory.

cd DependencyDemo

1. Create the Interface

And now, create our interface.

ng generate interface logger

Here's the code for our new interface.

export interface Logger {
    info(message: string): void;
    warning(warning: string): void;
    error(error: string): void;
}

2. Implement the New Interface

Now, we'll generate a service to implement our new interface.

ng generate service logger

And the code for our new service.

import { Injectable } from '@angular/core';
import { Logger } from './logger';

@Injectable({
  providedIn: 'root'
})
export class LoggerService implements Logger {

  constructor() { }

  info(message: string): void {
    console.info(message);
  }
  warning(warning: string): void {
    console.warn(warning);
  }
  error(error: string): void {
    console.error(error);
  }
}

3. Link Interface and Implementation

Last of all, we need to inject our interface along with it's implementation.

Open the app.module.ts file and edit the providers array to look like this.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { LoggerService } from './logger.service';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [
    {
      provide: 'Logger', useClass: LoggerService
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

4. Using The Logger

And last of all, we can grab an instance of our interface and use it like this.

import { Component, Inject, OnInit } from '@angular/core';
import { Logger } from './logger';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent  implements OnInit {

  constructor(@Inject('Logger') public logger: Logger) { }

  ngOnInit(): void {
    this.logger.info("AppComponent")  
  }
}

Do I have to wrap dependencies?

Maybe you're wondering if you have to wrap dependencies.

Is this really worth the effort?

Recently I did some consulting work for a startup called Relion. They had a slow Angular app that was causing browser freezes and needed help debugging the issue.

After doing some profiling I discovered that part of the slowness was caused by a 3rd-party library they were using to sync with Firebase.

If they hadn't wrapped the dependency I might still be working there today. But because they had followed this rule of wrapping dependencies with an interface, it was so much easier to discard the library and find a better solution. All we had to do was change some implementation details without breaking the rest of the app that referenced the interface instead of the implementation.

Is wrapping dependencies worth it? Absolutely!

But maybe you still disagree? Let me know in the comments below. 👇

signature

Angular Consultant

P.S. - If you're a skimmer like me than here's what this article is all about.

  • A story about a rogue developer that shows why you shouldn't blindly import any library.
  • Help you decide if you need to wrap a specific dependency (library, module, etc...) or not.
  • Ways you can wrap a dependency to avoid breaking your Angular app.

References