To get started with dependency injection, we first want to understand what it's actually all about. Let's start off with this class. The class car needs two other services to be constructed, engine and doors. What we do is, we import engine and doors, define an engine and doors property of their dedicated types, and assign instances to them in the constructor using the new keyword.
Then there's the method start engine which simply calls this.engine.start. By taking a look at the engine class, we can see that start really just locks some text. This code works perfectly fine. We can go into our main TS file and create an instance of car, using let car = new car. Once the instance is created, we can call start-engine on it.
We save the file, reload the browser, and see our engines are started. This is great. It turns out that there are a couple of problems with this class in terms of maintainability, testability, and scalability. Because car knows exactly how to create instances of engine and doors, it is very hard to use this class in a different environment where it might need different dependency implementations.
In addition, it is now rather hard to write isolated unit tests for car because there's no simple way to swap out engine and doors with mock classes. That's exactly where dependency injection comes into play.
Dependency injection means that all instances of needed dependencies to construct an object are passed to the objects constructor. In terms of code, this means that we change our car constructor to simply ask for its dependencies instead of creating them.
We now literally moved the responsibility of creating dependencies to a higher level. Car knows nothing about how to create its dependencies, and that's a good thing because we can now swap out its dependencies with mock classes when we write unit tests.
If we were writing types grid, which is the case, we can simplify this code by using either the public or private keyword. This is a shorthand syntax for assigning dependencies to class properties with the same name. Now, if we come back to our main function, we need to update it as it's now responsible for creating all objects.
This works fine, but we usually deal with way more objects and larger applications. At this point, maintaining all the dependencies can be quite hairy, and that's why Angular comes with a built in DI system that takes care of that for us.