Dependency Injection in Angular

Dependency Injection in Angular

So I'm reading up on Angular these days, and one of the cool things about it is Dependency Injection. As a Spring developer, I'm quite familiar with the inversion of control and how it can make your applications much more flexible and easier to test. If you aren't familiar with it, I suggest you look it up on Wikipedia. The basic idea, however, is that you define a type like a car, and inject into it the various dependencies rather than building them inside. The Angular analogy in TypeScript looks something like this:


@Injectable
class Car {
  constructor(private engine: Engine, private wheels: Wheel[], ...) {
    ...
  }
}

By annotating the class with the @Injectable annotation, you are basically indicating that Angular should resolve dependencies from providers when this type is constructed. The example car above has several depencies like the engine, wheels, etc. In this way, the logic within the Car type doesn't have to worry about how these things are constructed or really what is inside of them. From the perspective of the Car, they are black boxes that expose an API.

The real power of dependency injection, however, is that because the items being injected are black boxes, the contents inside the boxes could be different each time a Car is created. The API exposed is the same, but imagine in one Car a V12 engine is injected and in another, a V6 is injected. The two engines work the same way from an API perspective, but they obviously function different internally. The black boxes could also be mock objects - that is, they don't actually have a real implementation at all. This is especially useful in testing scenarios where you don't really want the usual logic inside these boxes to be executed. If we are testing a component that normally makes calls to update a database, we may not actually have a real database to update, so we provide a mock instead.

Note that without the dependency injection pattern, doing tests in isolation becomes much harder. When the constructor for a Car must also construct the parts that make up the Car, you are tightly coupling the Car to those other parts. Either you always use the same type of engine, or you have a parameter that specifies the type of engine to use, but then the Car has to be aware of all the possible types of engine ahead of time. This effectively complicates the implementation of the Car because instead of just writing logic to handle requests to the Car API, you also have to write logic to deal with all these possible options.

The other part of dependency injection in Angular is the provider specification which basically defines the blueprint of what should be injected for given dependencies. In other words, the Car type defines an abstract representation of a car where parts of it are just left as details that can be specified later. The provider says "when they ask for an engine, provide a V12". Interestingly enough, although TypeScript is typed, with respect to dependency injection, you can say that when a dependency is of a given type, use a different type instead at runtime. Obviously if you pass some type that isn't compatible from an API perspective, you'll hit a runtime error, but there is no strict compile-time checking that the types are in fact compatible. For example, I can define my engine types like this:


class Engine { ... }

class V12Engine { ... }

class V6Engine { ... }

Then I can specify my providers (which are specified as part of the NgModule declaration) like this:


@NgModule({
  providers: [Car, {provide: Engine, useClass: V12Engine}],  ... })
...

Basically, from a type perspective, there is no real relationship between Engine and V12Engine, but you are allowed to inject one in the place of the other. Java, on the other hand, is much more strict about this, so you either have to use inheritance or use interfaces. Interfaces are preferred because classes can implement multiple interfaces but can only extend one base class. TypeScript has interfaces too, so I could define the Engine type as an interface and then make V12Engine and V6Engine extend it. Unfortunately, if you try this, it won't work. Interfaces are more of a compile-time mechanism in TypeScript, rather than a runtime one, and so you can't specify an interface as what is provided. There is a workaround, however, since you can specify a string that specifies the name of the interface and then annotate the parameter to the Car constructor to indicate the depdency is of the interface type. This is what it looks like:


@Injectable()
class Car {
  constructor(@Inject('Engine') private engine: Engine, private wheels: Wheel[], ...) {
    ...
  }
}

@NgModule({
    providers: [Car, { provide: 'Engine', useClass: V12Engine}], ... })
...

This satisfies Java folks like me who like interfaces, but it really isn't necessary. It does demonstrate, however, that there is more than one way to specify the dependencies that need to be injected. There is more to dependency injection in Angular, but it is beyond the scope of this post. Hopefully you got the high-level idea and it makes sense why frameworks like Angular have gotten such attention in the changing landscape of web development.

Related Article