09 Dec 2021
5 min

Angular & Open/Closed Principle

Introduction

This is the second part of the SOLID article series. It’s a set of principles that will not only allow us to write a code that scales more effortlessly but also change the behavior of our application without changing a large part of the application code. 

The following set of rules consists of: 

Today we’ll focus on the Open/Closed Principle 🙂

Open/Closed Principle

Credit: https://maksimivanov.com/posts/open-closed-principle/

As we can see in the picture above – poor Edward Scissorhands, only has scissors at his disposal. It would be far better if he could use other tools as well. How can it be done? Maybe he should simply take the scissors into his hands instead of attaching them to his body permanently. 🙂

Moving on to the definition of this rule: class/object should be open for expansion, but closed for modification. What does it mean? In brief – you need to write the code in such a way that new functionalities can be introduced without modifying the existing code.

Initially, it referred to the realization of polymorphism (changing the objects’ behavior) through inheritance, i.e. creating a base class and inheriting from it.

Then:

  • the base class is closed to modifications by the extending class because from the level of the extending class we have no influence on the methods of the base class,
  • the base class is open to expansion because we can extend it through inheritance and extend its functionality.

But inheritance introduces a strict dependency between objects (between the base class and its subclasses). Remember, creating a code, we should try to avoid strict dependencies. 

Otherwise:

  • we make it difficult to mock dependencies and to write tests,
  • we also make it difficult to swap implementations (it requires changes in many classes),
  • we create code that is less susceptible to change (one big dependency monolith is created – changing one thing entails changing many others). 

Therefore, it is better to use interfaces instead of inheritance. Why? 

  • we can easily change individual implementations (thus changing the behavior of the application), without changing the rest of the code,
  • interfaces are closed for modifications, but open for expansion (we change the behavior by adding a new implementation of an existing interface),
  • when using interfaces, we are not dependent on the implementation/fields defined in the base class (so we are more independent),
  • we introduce an additional layer of abstraction that allows objects to be "loosely" bound.

Example

Recently, we have been developing a new version of an existing application module. However, we didn't want the new version to be visible to every user. We wanted to limit access to this new module for only a few trusted clients. In order to realize this, we decided to use the so-called feature flags mechanism. 

It works like this: the backend (and theoretically also the frontend) provides information which functionalities are enabled for a particular user and which aren’t. This is an object that contains multiple key-value fields. If we get a value of "TRUE" in any of the fields, the given functionality should be enabled.

To check access to the newly created module, we used the guard, which checked whether the feature flag allows us to access it or not. Therefore, we have created an enum that describes the modules, and also a website that has a method to check access to a new module:

export enum AppModule {
    ORDER,
    ORDER_NEW,
    CUSTOMER
}

@Injectable()
export class ModuleAccessService {
    hasAccessToNewOrderModule(): boolean {}
}

In our guard, we call the hasAccessToNewOrderModule method and based on its result, we enable the transition to a new module or we block it. On the surface, everything seems fine – the code is working. 

So where's the problem?

What if we would like to check the access to other modules? We would need to modify the existing service by adding another method. This way our file with this service could grow to many, many lines. Besides, either the guard would have to know all these methods, or we would have to create multiple guards, each per module, and we would have to know, which method to call. 

Certainly, some of you have already encountered files with hundreds of lines of code in your career. If not, believe me, you wouldn't want to modify something in that kind of code afterwards ? To avoid this situation, let's modify this code to a better form:

export enum AppModule {
    ORDER,
    ORDER_NEW,
    CUSTOMER
}

@Injectable()
export class ModuleAccessService {
    hasAccessToModule(module: AppModule): boolean {}
}

If there was a new module to which we would like to check access, it would be enough to add a line in the enum and use the new, more generic service method. 

However, this still wouldn't be perfect – (probably) we’d have to modify the big switch that would be inside of this method. Even though we could already have one common guard that uses the generic method, we would still need to pass the appropriate enum’s value to it.

So, let's modify this code even further and isolate the interface:

export abstract class ModuleAccessService {
    abstract hasAccess(): boolean;
}

Now, for each module that we would like to check access to, we create a new implementation:

@Injectable()
export class OrderNewModuleAccessService implements ModuleAccessService {
    hasAccess(): boolean {
        // do stuff
    }
}

From now on, we use abstractions in our guards (not a specific implementation):

@Injectable()
export class OrderNewModuleGuard implements CanActivate {
    constructor(private accessService: ModuleAccessService) {}

    canActivate(): boolean {
        return this.accessService.hasAccess();
    }
}

We provide a specific implementation, e.g. at the module level:

@NgModule({
    providers: [
        {
            provide: ModuleAccessService,
            useClass: OrderNewModuleAccessService,
        },
        OrderNewModuleGuard,
    ]
})
export class OrderNewModule {}

This is how we stick to the Open / Closed Principle. In the next article, we'll look at the Liskov Substitution Principle ?.

Frequently asked questions

What is the Open/Closed Principle in Angular?

The Open/Closed Principle means a class should be open for extension but closed for modification, so new behavior arrives without editing existing code. In Angular, you achieve this by depending on an abstraction such as an abstract class like ModuleAccessService, then providing a new implementation per feature. Guards then stay unchanged when you add functionality.

Should I use interfaces or inheritance for the Open/Closed Principle in Angular?

Prefer interfaces over inheritance. Inheritance creates a strict dependency between a base class and its subclasses, which makes mocking, testing, and swapping implementations harder. With an abstraction like abstract class ModuleAccessService and implements, each implementation is independent of the base class fields and methods, so you change behavior by adding a new implementation rather than modifying existing classes.

How do I swap a service implementation per module with useClass in Angular?

Provide the abstraction token with useClass at the module level, for example { provide: ModuleAccessService, useClass: OrderNewModuleAccessService } in the providers of OrderNewModule. The guard injects ModuleAccessService through its constructor and calls hasAccess() without knowing the concrete class. Changing the implementation then only requires editing the provider configuration, not the guard.

How do I implement feature flags with a route guard in Angular?

Use a route guard that implements CanActivate and returns a boolean based on the feature flag for that module. The backend supplies an object of key-value fields, and a value of TRUE in any field enables the functionality. Put the check behind an abstraction such as ModuleAccessService, so the guard only calls hasAccess() and each new module adds an implementation instead of extending one large service.

Why does a service with one method per module become hard to maintain in Angular?

Adding a method per module makes the service file grow to many lines, and every new check forces you to modify existing code. Either the guard must know every method, or you need one guard per module that calls the right method. A generic hasAccessToModule(module: AppModule) reduces this but still needs a large switch, so isolating each check behind an interface removes that growth entirely.

Share this post

Sign up for our newsletter

Stay up-to-date with the trends and be a part of a thriving community.