Web Analytics Made
Easy - StatCounter

Menu

About us Contact us

Making Configurable Angular Feature Modules Using Strategy Pattern

Tech 2019 / 08 / 22

Making Configurable Angular Feature Modules Using Strategy Pattern



House cleaning takes effort, but the result is priceless. Same goes for clean code. It takes effort, but patterns definitely increase the life expectancy of code. When we create some Angular feature modules and want to reuse them, we often end up rewriting the module either by updating the config or logic inside some classes to fit our needs. We should try our best to avoid these sort of practises as much as possible for better code maintenance.

One of the aspects of clean code is to ensure that our code is closed for modification but open for extension. Let’s make a service feature modulethat will encapsulate the feature strategies and depending on the use case, allow us to choose the appropriate strategies without any modification.

Today for this article’s demonstration, we will create a classic session manager that has multiple type of storage classes. Each storage class can access a specific storage like local storage, or session or cookie storage. When this session manager is eagerly loaded by the root module, one can pass in the configuration as to which storage it will be using during runtime.

Photo by JESHOOTS.COM on Unsplash

The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

Before we dive into the Angular code, here is the UML diagram of how we are planning to implement a session manager using strategy pattern.

UML Diagram for SessionManager

SessionStrategy is an interface that is implemented by storage classes like LocalStorage and SessionStorage. SessionContext class will just refer this strategy interface, thus making SessionContext class independent of how strategies are implemented.

We will then be creating a client, whose responsibility would be to initiate a specific session strategy object, like LocalStorage/SessionStorage and pass it to the context. It would be something like this:

const context = new SessionContext(<YOUR-PREFFERED-STORAGE-CONFIG-CLASS>);
const session = context.loadStorage();
const token = session.get('access_token');

The benefits of using strategy pattern here is that we have separated the concerns, can add on different storage classes with time and avoided complex conditions to initiate them every time.

Now let’s jump to Angular now. But before that, I think we have seen this code so many times:

const routes: Routes = [{
    path: 'heroes',
    component: HeroesComponent
}];
@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule { }

We can see a RouterModule forRoot function which accept routes of type Routes, that we can define in every project.

type Routes = Route[];

So basically the router takes an array of routes and a URL, and tries to create a RouterState. All this with an intention that application can render the component when navigating to a matching url.

The point of sharing this piece of code is to showcase how forRoot() takes a service configuration object and returns a ModuleWithProviders, which is a simple object with the properties like ngModule, and providers.

Let’s write our SessionManagerModule now. It should either access local storage or session storage based on our configurations. First, we create a session-manager module, and create this folder structure within it. I will explain the purpose of these folders as I go along.

session-manager
  - config  
  - storages
  - strategy
  session-manager.module.ts
  session.service.ts

So in the storages folder, we will have our two storage classes, LocalStorage and SessionStorage. Each class will have functions to read/write the browser storages.

However, before writing those classes, let us create an interface called SessionStrategy that will be implemented by both the classes.

session-manager
  - config  
  - storages
  - strategy 
       - session-strategy.ts
  session-manager.module.ts
  session.service.ts

We created a session-strategy.ts file and and now let’s write that class.

export interface SessionStrategy {
  get(key: string);
  set(key: string, value: string | Object);
  remove(key: string);
  removeAll();
}

Now we got back to the storages folder and create two more classes:

session-manager
  - config  
  - storages
      - local-storage.ts
      - session-storage.ts  
  - strategy 
      - session-strategy.ts
  session-manager.module.ts
  session.service.ts

We will be implementing the SessionStrategy in our two new classes:

import { SessionStrategy } from '../strategy/session-strategy';
export class LocalStorage implements SessionStrategy {
    static setup(): any {
        return LocalStorage;
    }
    public getStorage() {
        return window.localStorage;
    }
    public get(key: string): any {
       return this.getStorage().getItem(key);
    }
...

And the SessionStorage class:

import { SessionStrategy } from '../strategy/session-strategy';
export class SessionStorage implements SessionStrategy {
     static setup(): any {
        return SessionStorage;
     }
     public getStorage() {
        return window.sessionStorage;
     }
    public get(key: string): any { 
        return this.getStorage().getItem(key);
    }
...

Now let’s create a context class file in the strategy folder.

session-manager
  - config  
  - storages
       - local-storage.ts
       - session-storage.ts
  - strategy 
       - session-strategy.ts
       - session-context.ts
  session-manager.module.ts
  session.service.ts

Context class will just refer the strategy interface for executing the strategy and its functions.

import { SessionStrategy } from './session-strategy';
export class SessionContext {
    public sessionStrategy: SessionStrategy;
    constructor(sessionStrategy: SessionStrategy) {
        this.sessionStrategy = sessionStrategy;
   }
   
   public loadStorage() {
       return this.sessionStrategy;
  }
}

Now we will be creating a client, i.e. a specific session strategy object and pass it to the context. Our SessionManagerModule will replace the strategy associated with the context when we make our module configurable.

Photo by Randy Fath on Unsplash

We will be using Dependency Injection to make our module configurable. We will be using InjectionToken for this.

But first, we will be creating two files in the config folder. One an interface of the config called session-manager-config.ts and then create a token that implements it called session-manager-token.ts

session-manager
  - config  
      - session-manager-config.ts
      - session-manager-token.ts
  - storages
       - local-storage.ts
       - session-storage.ts
  - strategy 
      - session-strategy.ts
  session-manager.module.ts
  session.service.ts

In order to create InjectionToken, let’s define the type and the name of the token. Lets call it SessionManagerConfig and write that down in session-manager-config.ts.

import { SessionStrategy } from '../strategy/session-strategy';
export interface SessionManagerConfig {
    storage: SessionStrategy;
    // refreshTokenUrl: string;
}

Here you can see, we declared one interface that will have a storage class of SessionStrategy, and also it can have additional stings like refreshTokenUrl which is a string etc.. I just added it to show the variety of configs that we can use for DI.

So let write the token.

import { InjectionToken, ModuleWithProviders } from '@angular/core';
import { SessionManagerConfig } from './session-manager-config';
export const configService = new     
     InjectionToken<SessionManagerConfig>('config');

We thus created an InjectionToken of type SessionMangerConfig. Now, we can write our session.service.ts class which will act like our client as it will inject whatever strategy is injected via the module.

import { Injectable, Inject } from '@angular/core';
import { configService } from './config/session-manager-token';
import { SessionContext } from './strategy/session-context';
import { SessionStrategy } from './strategy/session-strategy';
@Injectable({
    providedIn: 'root'
})
export class SessionService {
    public static session: SessionStrategy;
    private context: SessionContext;
    constructor(@Inject(configService) public config, public http:       
        HttpClient) {
        const storage = new config.storage();
        this.context = new SessionContext(config.storage);
        SessionService.session = this.context.loadStorage();
     }
     public getAccessToken(): string {
          return SessionService.session.get('access_token');
     }
     public setAccessToken(value: string): void {
          return SessionService.session.set('access_token', value);
     }
...

Here you can see in the constructor, a context is initiated based on a config. This config which is the storage object would come from the module configuration. Depending on how you declared the SessionManagerModule, getRefreshToken will will fetch refresh_token value from either localStorage or SessionStorage.

Let us move to our SessionManagerModule, and declare the forRoot() function and expect one parameter called config of type SessionManagerConfig that we declared earlier.

import { SessionService } from './session.service';import { SessionManagerConfig } from './config/session-manager-config';
import { configService } from './config/session-manager-token';
import { LocalStorage } from './storages/local-storage';
import { SessionStorage } from './storages/session-storage';
export class SessionManagerModule {
    static forRoot(config: SessionManagerConfig) : ModuleWithProviders {
        return {
            ngModule: SessionManagerModule,
            providers: [
                LocalStorage,
                SessionStorage,
                
                SessionService,
               {
                   provide: configService,
                   useValue: config || null
               }
           ]
      }; 
   }
}

So now we update the AppModule and show how to use this SessionManagerModule.

import { SessionManagerModule } from './session-manager/session-manager.module';
import { SessionManagerConfig } from './session-manager/config/session-manager-config';
import { LocalStorage } from './session-manager/storages/local-storage';
import { SessionStorage } from './session-manager/storages/session-storage';
const sessionConfig: SessionManagerConfig = {
    storage: SessionStorage.setup()
};
@NgModule({
    imports: [
         SessionManagerModule.forRoot(sessionConfig),
    ],
    declarations: [AppComponent],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule {}

Here we could have passed a class instance of SessionStorage had we used JIT complier, but in AOT it would not work:

const sessionConfig: SessionManagerConfig = {
    storage: new SessionStorage()
};

Since in AOT, function calls are not supported in decorators, we call the static function setup so to ensure AOT build passes.

const sessionConfig: SessionManagerConfig = {
     storage: SessionStorage.setup()
};

So at the end, depending on how you declared the SessionManagerModule, getRefreshToken will will fetch refresh_token value from either localStorage or SessionStorage.

You can see the code in my GitHub repo, and edit the code directly using Stackblitz:

StackBlitz

Saad-Amjad/session-manager

In conclusion, there are many ways for dependency injection in Angular, for this article we showed you the usage of InjectionToken. In order to configure a module, we used forRoot() and passed in configuration. You can checkout the Angular Docs to understand when to use forRoot()/forChild() in your applications. Instead of complex switch statements, to make swappable implementation of the storages that we get from config, we used strategy pattern, ensuring the module is open for extension but closed for modification.

Feel free to reach out to me on Twitter (@saadbinamjad), and you can check some other articles posted by me on Medium and our Engineering Blog.

Saad Bin Amjad – Medium

If you want to read more on some diversified topics, please checkout our company blog. Thanks, till then happy coding!

You have ideas, We have solutions.

CONTACT US!