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:

  1. The custom ErrorHandler must be injected into the component which will implement the mode switching:

     constructor(public errorHandler: ErrorHandler) { }
    
  2. The CustomErrorHandlerService must be declared as the provider for the ErrorHandler service in AppModule:

     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 the CustomErrorHandlerService.
  • The Strategy interface which is the common interface of all the implementations. In our case, this is the ErrorHandler.
  • Multiple classes which implement the Strategy interface in a different way. In our case, this will be the LocalErrorHandlerStrategy and the RemoteErrorHandlerStrategy.

The following UML diagram describes the relations between them:

Strategy software design pattern

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.

Copyright
Creative Commons License