Switching Angular Services at Runtime
A while ago I've already written a blogpost on how to inject a different Angular service implementation based on a runtime value. With that approach, the selected service was initialized at startup and remained the same for the entire application lifetime. In response to that blogpost, I received a question how one could switch between the implementations while the application is running. This blogpost is my detailed answer to that question.
At least to my knowledge, there's no built-in support for such functionality in Angular's dependency injection implementation. This means that from Angular's point of view the same service will be in use the whole time. The switching logic will have to be implemented inside that service.
Encapsulating Both Implementations in a Single Service
The simplest approach would be to include both implementations inside that single service. For example, the following custom ErrorHandler
service can switch between local and remote error reporting at runtime:
import { Injectable, ErrorHandler } from '@angular/core';
export type ErrorHandlerMode = 'local' | 'remote';
@Injectable()
export class CustomErrorHandlerService implements ErrorHandler {
mode: ErrorHandlerMode = 'local';
constructor() { }
handleError(error: any): void {
switch (this.mode) {
case 'local':
this.handleErrorLocal(error);
break;
case 'remote':
this.handleErrorRemote(error);
break;
}
}
private handleErrorLocal(error: any) {
console.error(error);
}
private handleErrorRemote(error: any) {
console.log('Send error to remote service.');
}
}
The handleError
method delegates the call to the appropriate implementation based on the current value of the mode
property. The value of the property can change at runtime. It can even be bound to an input
element:
<div>
<input name="mode" type="radio" id="local" value="local"
[(ngModel)]="errorHandler.mode">
<label for="local">Local error reporting</label>
</div>
<div>
<input name="mode" type="radio" id="remote" value="remote"
[(ngModel)]="errorHandler.mode">
<label for="remote">Remote error reporting</label>
</div>
There are two more prerequisites for this to work:
The custom
ErrorHandler
must be injected into the component which will implement the mode switching:constructor(public errorHandler: ErrorHandler) { }
The
CustomErrorHandlerService
must be declared as the provider for theErrorHandler
service inAppModule
:providers: [ { provide: ErrorHandler, useClass: CustomErrorHandlerService } ],
For simple services, this approach can be good enough. But as the number of methods in the service increases, repeating the switching logic in each one and having to keep everything in a single class will make maintenance more difficult.
Implementing the Strategy Pattern
The Strategy software design pattern is a standard approach for dynamically selecting an algorithm (or an implementation in our case) at runtime. It consists of:
- The
Context
class which delegates the calls to the correct implementation. In our case, this is theCustomErrorHandlerService
. - The
Strategy
interface which is the common interface of all the implementations. In our case, this is theErrorHandler
. - Multiple classes which implement the
Strategy
interface in a different way. In our case, this will be theLocalErrorHandlerStrategy
and theRemoteErrorHandlerStrategy
.
The following UML diagram describes the relations between them:
Let's take a look at the code. The handleError
method now simply calls the corresponding method in the currently selected strategy. I implemented the switching of strategies in the mode
property setter:
import { LocalErrorHandlerStrategy } from './local-error-handler-strategy.service';
import { Injectable, ErrorHandler } from '@angular/core';
import { RemoteErrorHandlerStrategy } from './remote-error-handler-strategy.service';
export type ErrorHandlerMode = 'local' | 'remote';
@Injectable()
export class CustomErrorHandlerService implements ErrorHandler {
private modeValue: ErrorHandlerMode;
private currentStrategy: ErrorHandler;
get mode(): ErrorHandlerMode {
return this.modeValue;
}
set mode(value: ErrorHandlerMode) {
this.modeValue = value;
switch (value) {
case 'local':
this.currentStrategy = this.localStrategy;
break;
case 'remote':
this.currentStrategy = this.remoteStrategy;
}
}
constructor(
private localStrategy: LocalErrorHandlerStrategy,
private remoteStrategy: RemoteErrorHandlerStrategy) {
this.mode = 'local';
}
handleError(error: any): void {
this.currentStrategy.handleError(error);
}
}
The two strategies are also implemented as Angular services and provided by dependency injection. If I didn't have to do any switching at runtime they could be used as standard services on their own:
import { Injectable, ErrorHandler } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class LocalErrorHandlerStrategy implements ErrorHandler {
constructor() { }
handleError(error: any): void {
console.error(error);
}
}
import { Injectable, ErrorHandler } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class RemoteErrorHandlerStrategy implements ErrorHandler {
constructor() { }
handleError(error: any): void {
console.log('Send error to remote service.');
}
}
The rest of the code remains the same as in the first approach. The Strategy pattern does not affect the public interface of the CustomErrorHandlerService
. It only changes how the switching is handled internally allowing proper separation between the different implementations.