Application modularity requires that chunks of code performing the same task in several Angular components be factored out into separate classes. Actually, to improve software maintainability, it is best practice to factor out all code chunks that might be useful to other Angular component classes even when these other components have not yet been created. As a consequence, Angular components should only contain code that is specific to their internal peculiarities, that is, the code that defines their interaction with the user. Code that defines business logic, and localization rules, as well as code that handles application-level services, must be factored out in various specialized classes, which are usually referred to as services.
Thus, for instance, the code that takes care of retrieving and updating products from a database located on the server should be grouped in dedicated service classes. In general, the whole business logic that updates and retrieves data handled by a component must be factored out in service classes. Also, general-purpose error messages should be factored out in services, since they are application-level resources.
The simple approach of letting each component create all the services they need has several drawbacks:
- Service settings usually depend on the overall application state, which is unknown to each single component. Therefore, in general, components should be able to create services with the correct settings, so forcing them to do it would break application modularity, and would yield spaghetti code.
Services that depend on the context are more common than they might appear, since possible contexts also include component testing. During unit testing, it is very difficult to arrange fake servers and other application parts that behave as required by each test. Therefore, when testing components, fake services are used that provide the data needed by the tests without actually interacting with a server and/or other application parts. - Service creations may be expensive operations, so it is convenient that several components share the same service instance. Moreover, instance sharing becomes a necessity when service instances contain state information that must be shared among components.
Implementing shared instances with static properties directly handled by the components breaks application modularity, creates problems during testing, and is likely to yield hard-to-find bugs during software maintenance because access to the static properties containing the shared services are spread throughout the code: they are hidden inputs/outputs that are difficult to track, and difficult to prepare, during testing.
Dependency Injection (DI) was conceived to overcome all of the aforementioned drawbacks. According to the DI paradigm, service instances are handled by a hierarchy of containers. Each container has a record for each service it can provide.
This record contains information on the following:
- How to create each service in a way that can be dependent on the overall application state?
- If the service is a singleton, or if a new instance is created each time the service is requested. When a service is not found in a container, it is searched up in the container hierarchy until it reaches the root. Thus, the structure of the hierarchy defines which requests share the same singletons, and which requests share the same service creation strategy.
Angular, like all frameworks relying on DI, allows the automatic injection of services in class constructors. Some DI engines also allow services to be injected into adequately tagged properties. All DI frameworks allow the recursive injection of services, that is, services mentioned in the constructor of other services are automatically created and injected into the service constructor, and this process is recursive. All requests caused by the recursion on the same initial request are resolved in the same container of the initial request (moving up in the containers hierarchy if the service is not found).
Angular containers are called service injectors, or simply injectors, and they contain only singletons; in other words, each service instance, once created in a container, is reused for all subsequent requests issued to that container. This is not an actual limitation, since Angular is a client-side framework, so singleton here means unique to the user session. Moreover, injector hierarchy also includes injectors associated with each component instance, so service instances placed there are unique, but only in the lifetime of that component instance.
Angular injects services in the constructors of all Angular building blocks: components, directives, pipes, and services themselves. Each parameter in the constructor of these class types is interpreted as a service retrieved in the current container, and injected into the constructor before creating an instance. The next subsection explains how the hierarchy of Angular injectors is organized and how to determine the current injector when components, directives, and pipes are created. Services mentioned in the constructor of other services use the same container as the initial request that caused their creation.
Angular contains a unique platform injector that is populated when the application is bootstrapped. This injector contains services used by the framework itself, and other services that the developer can add in main.ts where the application is bootstrapped. The code scaffolded by the Visual Studio Angular template adds a service that returns the application base URL, as follows:
export function getBaseUrl() {
return document.getElementsByTagName('base')[0].href;
}
const providers = [
{ provide: 'BASE_URL', useFactory: getBaseUrl, deps: [] }
];
platformBrowserDynamic(providers).bootstrapModule(AppModule)
.catch(err => console.log(err)); In this case, the service is indexed by the string token BASE_URL. The singleton is itself a string that is computed by a factory function. The following subsection discusses in detail all the options Angular offers to index and select services, and to create their singleton instances.
Other injectors are also added to all instances of Angular modules and components. The records that describe all services provided by each injector are called service providers, or simply providers. They are added to the providers array property of the @NgModule and @Component decorators:
@NgModule({
...
providers: [
{...},
{...}
...
],
...
})
...
@Component({
...
providers: [
...
],
...
}) These injectors have the same lifetime of modules and component instances of which they are part. Module instances live until the end of the application. Accordingly, injectors associated with module instances span the entire lifetime of the application. Component instances and their injectors have a shorter lifetime, since they are removed from the DOM when the user navigates to other content, or when some DOM parts are destroyed for other reasons. For instance, components contained in an *ngIf directive are removed with all their descendants when the *ngIf condition becomes false. Components contained in an *ngFor instance are destroyed when the enumerable bound to the directive changes. Components contained in a <router-outlet> instance are removed with all their descendants when a router loads a different component instance in that <router-outlet>. Moreover, while modules are singletons, components may be instantiated several times in different places. Thus, injectors associated with components have a lifetime that is limited to the lifetime of each component instance, and that is usually shorter than the lifetime of the application.
All services added to each x module are merged into the application main module injector (app.component.ts) when the module is imported. Thus, when the application starts, there is just one a...