Using Abstract Classes As Dependency-Injection Tokens For Swappable Behaviors In Angular 4.2.3

Over the weekend, I was looking at ways to cleanly encapsulate Firebase's realtime API behind a stream-based data access layer (DAL). As part of that exploration, I had to provide multiple gateway implementations (or behaviors) that could be swapped in and out seamlessly. At first, I wasn't quite sure how to accomplish this in Angular 4. After some trial and error, though, I discovered that I could use an Abstract class as the dependency-injection token for my swappable services. This felt like an elegant approach for interchangeable services.

My first instinct was to use a TypeScript Interface to define the behavior of the swappable classes. Unfortunately, you can't use an Interface as a dependency-injection token in Angular 4 since an Interface doesn't actually have a runtime artifact (it's only used during compile-time to drive type-safety). As such, my first attempt used an InjectionToken do define constructor-argument meta-data:

// Import the core angular services. import { Component } from "@angular/core"; import { Inject } from "@angular/core"; import { InjectionToken } from "@angular/core"; // ----------------------------------------------------------------------------------- // // ----------------------------------------------------------------------------------- // // This injection token can be used to configure the Providers as well as the @Inject() // meta-data that will tell the dependency-injection container how to locate the desired // class instance. var GreeterToken = new InjectionToken<GreeterInterface>( "Greeter behavior" ); interface GreeterInterface { greeting() : string; } class NiceGreeter implements GreeterInterface { public greeting() : string { return( "Hello, what a pleasure to meet you." ); } } class MeanGreeter implements GreeterInterface { public greeting() : string { return( "Hello, you are a doofus!" ); } } // ----------------------------------------------------------------------------------- // // ----------------------------------------------------------------------------------- // @Component({ selector: "my-app", providers: [ // For this application, let's provide the MeanGreeter instance when the // GreeterToken needs to be injected into the App component. { provide: GreeterToken, useClass: MeanGreeter // <--- Defining the swappable implementation. } ], styleUrls: [ "./app.component.css" ], template: ` <p> <strong>Greeting:</strong> {{ greeting }} </p> ` }) export class AppComponent { public greeting: string; // I initialize the app component. constructor( @Inject( GreeterToken ) greeter: GreeterInterface ) { this.greeting = greeter.greeting(); } }

As you can see, in the AppComponent constructor(), I have to use the InjectionToken to define an @Inject() decorator. This tells the Angular dependency-injection container what I'm actually trying to inject for the given "greeter" argument. And, when we run this version of the app, we get the following output:

As you can see, this approach works. But, I don't like it. There's something that feels janky about having to use the @Inject() decorator in order to inject a Class instance. After all, the whole dependency-injection system revolves around Class instances.

My next thought was to try to have one class "implement" the other class. This way, I could use the "base" class as the dependency-injection token:

// Import the core angular services. import { Component } from "@angular/core"; // ----------------------------------------------------------------------------------- // // ----------------------------------------------------------------------------------- // interface GreeterInterface { greeting() : string; } class NiceGreeter implements GreeterInterface { public greeting() : string { return( "Hello, what a pleasure to meet you." ); } } // CAUTION: Here, I am saying that the "MeanGreeter" IMPLEMENTS the "NiceGreeter." class MeanGreeter implements NiceGreeter { public greeting() : string { return( "Hello, you are a doofus!" ); } } // ----------------------------------------------------------------------------------- // // ----------------------------------------------------------------------------------- // @Component({ selector: "my-app", providers: [ // For this application, let's provide the MeanGreeter instance when the // NiceGreeter needs to be injected into the App component. { provide: NiceGreeter, useClass: MeanGreeter // <--- Defining the swappable implementation. } ], styleUrls: [ "./app.component.css" ], template: ` <p> <strong>Greeting:</strong> {{ greeting }} </p> ` }) export class AppComponent { public greeting: string; // I initialize the app component. constructor( greeter: NiceGreeter ) { this.greeting = greeter.greeting(); } }

As you can see, in this approach, the MeanGreeter class is "implementing" the NiceGreeter class. This is simpler than the InjectionToken approach. And, it works at first. But, I don't like it. There's something that seems janky about asking for a NiceGreeter type and receiving a MeanGreeter instance. The semantics of it don't sit right.

And, it really doesn't matter that it feels janky because it turns out to be a very brittle approach. Having one class "implement" another class breaks the moment you add a private property to the base class. Imagine that I wanted to refactor the NiceGreeter class such that it stored the greeting value as a private property:

class NiceGreeter implements GreeterInterface { private value: string = "Hello, what a pleasure to meet you."; public greeting() : string { return( this.value ); } } // CAUTION: Here, I am saying that the "MeanGreeter" IMPLEMENTS the "NiceGreeter." class MeanGreeter implements NiceGreeter { public greeting() : string { return( "Hello, you are a doofus!" ); } }

At this point, the TypeScript compiler starts to complain:

Error TS2420: Class 'MeanGreeter' incorrectly implements interface 'NiceGreeter'.

Property 'value' is missing in type 'MeanGreeter'.

In TypeScript, when one class implements another class, it has to implement all of its members, not just the public methods. That includes the private properties and the private methods. Which is, of course, antithetical to the very meaning of "private." As such, this approach becomes a non-starter.

The approach that I finally settled on was using an Abstract class to define both the interface and the dependency-injection token for the swappable behaviors:

// Import the core angular services. import { Component } from "@angular/core"; // ----------------------------------------------------------------------------------- // // ----------------------------------------------------------------------------------- // // Because an Abstract class has a runtime representation, we can use it as a // dependency-injection (DI) token in Angular's DI container. And, since each concrete // class has to implement or extend this abstract base class, it means that the base // class can act as the "interface" to the behavior as well. abstract class Greeter { abstract greeting() : string; } // NOTE: We could have also used "extends Greeter" if Greeter provided base // functionality that needed to be shared with its concrete classes. class NiceGreeter implements Greeter { public greeting() : string { return( "Hello, what a pleasure to meet you." ); } } // NOTE: We could have also used "extends Greeter" if Greeter provided base // functionality that needed to be shared with its concrete classes. class MeanGreeter implements Greeter { public greeting() : string { return( "Hello, you are a doofus!" ); } } // ----------------------------------------------------------------------------------- // // ----------------------------------------------------------------------------------- // @Component({ selector: "my-app", providers: [ // For this application, let's provide the MeanGreeter instance when the // Greeter needs to be injected into the App component. { provide: Greeter, useClass: MeanGreeter // <--- Defining the swappable implementation. } ], styleUrls: [ "./app.component.css" ], template: ` <p> <strong>Greeting:</strong> {{ greeting }} </p> ` }) export class AppComponent { public greeting: string; // I initialize the app component. constructor( greeter: Greeter ) { this.greeting = greeter.greeting(); } }

As you can see, this approach treats the Abstract Class much like an Interface that both the NiceGreeter and the MeanGreeter behaviors have to adhere to. The semantics of it also feel good. My App component asks for a "Greeter"; so, it doesn't feel janky to provide either of the sub-classes (if you will) as the runtime implementations.

In this particular case, I'm using the "implements" keyword since I don't actually want to inherit any behaviors from the base class; but, you could use the "extends" keywords if you wanted the Abstract Class to act as the inherited super class for the individual behaviors.

Now, to be clear, I'm not advocating that you start using abstract classes for all of your dependency-injection needs. I only came upon this approach because I needed to be able to swap behaviors within my own Angular 4 application. For this particular use-case, the abstract class seemed like an elegant solution.







