angularangular2-di

Dependency injection switch between implementations


I've got an app that's working in two different modes: demonstration and normal. The demo mode is only used when the user does not have an account and only wants to try the app to see how it works.

Now suppose that we have a service that's used in the app. For instance a service to retrieve an Account and authenticate a user:

abstract class AccountService {
  authenticate(username: string, password: string): Observable<Boolean>;
  getAccount(): Observable<Account>;
}

For this service we have two implementations, one for each mode. AccountServiceImpl, for the normal mode, performs an HTTP request to some server to retrieve the account. While the AccountServiceDemoImpl simply returns a hardcoded one.

Suppose we have an AccountComponent component that shows the account name to the user. For this component it does not matter which of the implementations we're using, so we just inject an AccountService in its constructor.

All is good and well, and we can easily use either of the implementations to fill out the AccountComponent. Now we want to be able to switch between these implementations at runtime when the user clicks a button on the login page. So I wrote a factory provider to determine which AccountService implementation to use

{provide: AccountService, deps[AccountServiceImpl, AccountServiceDemoImpl], (regularService, demoService) => demoModeActivated ? demoService : regularService}

As you can imagine, the AccountService is also injected on the login page to deal with the authentication of the user. Which means that the factory has already been evaluated. Now, the problem is that the result of this evaluation is cached in Angular. So when the demo mode is activated and the user navigates to the AccountComponent, the actual AccountServiceImpl will be injected again.

Is there a way to clear the DI cache (for a specific set of DI tokens)? Or should I try to deal with this feature in another way? (For instance, write another implementation of the AccountService that delegates to either the AccountServiceImpl or the AccountServiceDemoImpl.)


Solution

  • Initially it would seem that a factory provider would solve this for you. Unfortunately factory revolvers are synchronous and you are relying on an asynchronous call to determine which service to provide.

    Instead you can use a Resolve guard. Within the resolve method you would return an observable that calls the auth method and then maps to newing up either service. And then calls the first() operator to complete the observable after its first go.

    It resolve guards seem to have been designed to return simple DTO structures, not services. But this works.

    Instead of newing up instances, you can inject both services in the Resolve Guard and map to either one, in the case you want this service to remain a singleton.

    export class AccountServiceResolver implements Resolve<AccountService> {
    
      constructor(private accountService: AccountService, demmoService: AccountDemoService) { }
    
      resolve(): Observable<AccountService> {
    
        return this.accountService.getAccount().map(account => {
    
          if (account.real) {
            return this.accountService;
          } else {
            return this.demoService;
          }
        }).first();
      }
    
    }
    

    Then get this in your component:

    ngOnInit(route: ActivatedRoute) {
    
      const service = route.data[0];
    }