When S.O.L.I.D Met Frontend Components

Design patterns are in constant debate among frontend programmers and architects. When implemented correctly, they support better stability and maintainability for the code. They also save programming time and streamline the working process. In this paper I explore the possibility of using S.O.L.I.D principles as a design pattern in the frontend realm. I demonstrate the benefits of such process by sharing a real time example from the CyberArk code.

Shiri Haim
Oct 14, 2018 · 11 min read

A. Introduction

In a lecture, I delivered last week in Reversim conference at Tel-Aviv University, I discussed the possible implementation of S.O.L.I.D principles in the frontend realm. In this author’s view, the meeting between the S.O.L.I.D principle (mainly used so far to improve backend code) and frontend can lead to varies rewarding result in the time and quality of the code.

While discussing the topic with varies colleagues and participants, I discovered that the issue of creating a set of harmonized principles for frontend programming, bothers and also a source of enthusiasm among developers and architectures.

I would like to share here my thoughts and suggestion with regard to those topics and the role I believe S.O.L.I.D principles can take in it. I will provide my examples in Angular, even though they most definitely easily implemented on other frameworks.

B. The need of structured design patterns when coding frontend

Last year we initiated a strategic process in CyberArk with the goal of improving and harmonizing the developing process in the frontend. Our first goal was to adjust our frontend coding process to the reality of ongoing growing number of frontend developers, which take part in our web applications. Our second goal was to be able to update and extend our source code when the requirements change, easily, without effecting unrelated parts in the application. Our third goal was to streamline the coding process at large. In short, we wanted to achieve stability and maintainability in our project, and in our code, without increasing development time (at least not substantially).

Searching in my design patterns arsenal (including those I was familiar with from my backend time), the S.O.L.I.D principles jumped to my head. I thought it might be a good set of principles for achieving our goals.

At the same time, I was also deeply involved in a new frontend project in CyberArk, named Renaissance. Renaissance included a transformation of the main web application of CyberArk to Angular and WebAPI. In this context, it was necessary to rewrite all the frontend layer, and to expand and enrich our REST API and our internal web services. As the project grows larger, the number of frontend engineers dedicated to the project increased as well. As it happens in many cases like this, the more frontend engineers were coding to the same source code, the more it became fragile and unpredictable. Also at that period (as also happens in many cases like this), the product, UX and UI requirements change repeatedly. On each change, we needed to update our code, dealing with the (sometimes unknown) changes it may have.

While thinking of the strategic considerations of stability, maintainability, and shorter development time, which I mentioned before, I thought to myself that Renaissance can be the perfect environment to experiment with the combination between S.O.L.I.D and frontend.

C. What is S.O.L.I.D ?

Let get a quick reminder of the S.O.L.I.D principles. S.O.L.I.D are the first five principles of class design patterns, introduced by Robert C. Martin, which also known as Uncle Bob.

S.O.L.I.D stands for:

  1. Single Responsibility: Each software module should have one and only one reason to change.
  2. Open Closed: A module should be open for extension but closed for modification.
  3. Liskov Substitution: Subclasses should be substitutable for their base classes.
  4. Interface Segregation: Many client specific interfaces are better than one general purpose interface.
  5. Dependency Inversion: Depend upon Abstractions. Do not depend upon concretions.

The question remains whether those principles can be beneficially implemented in the frontend world.

D. Implementing S.O.L.I.D principles

Backing to the Renaissance project, I started to analyze one of the container components in our main default screen, named accounts. This container component named accounts-actions, allows to make actions on an entity named account. This can be achieved by clicking on action button inside the action menu. Each click on an action, will open a different dialog, which is related to the chosen action.

action menu in the accounts screen

This component contains hundreds lines of code and it is responsible for many processes. Among others, it is responsible for building the action menu dynamically (according to the http response of a configuration web service) and to manage the different actions of the account entity. It also contains the logic of each action. In such situation, changing one action logic, can influence in unpredictable way on other action logic, since all the actions logic are combined in one place. In addition, if we will have to use only one action logic in other place in our application, we will have to invest many resources for making it work. In the end of the day, it will become hard to maintain this component, and we will be reluctant to change things when needed.

Single Responsibility

When thinking about the code described above under this premise in head, it appears that we can achieve this goal quite easily.

By separating the long component to sub components classified by responsibilities, we now can use each sub component in other places in the application, and maybe even compose them, in a modular way. In addition, changing code in one of the sub components, will not affect the other. More than that, maintaining small components, which focusing only in one action, is easier, even for a developer which is not familiar with the code.

For example, the accounts-actions template, after splitting, will look as the following:

@Component({    selector: 'accounts-actions',    template: `        <action-menu [actions]="getActions() | async"                     (actionClicked)="currentAction = $event“>    
</action-menu>
<ng-container [ngSwitch]="currentAction“> <show-account-secret *ngSwitchCase="'Show'"> </show-account-secret> <connect-to-account-machine *ngSwitchCase="'Connect'"> </connect-to-account-machine> <change-account-secret *ngSwitchCase="'Change'"> </change-account-secret> </ng-container>`, styleUrls: ['./accounts-actions.component.css']})

We created a component which is responsible for building the action menu, and a component for each action. And now, the accounts-actions is responsible only for the integration between those sub components.

Open Closed

As the project expanded, more screens which represents different entities were created. For example, Requests screen, which display a list of user requests, to make some actions on a specific account. Also on this screen, we can find an action menu and dialogs, for each request, but the actions and the dialogs are different. A tempting solution to this issue, is to add the new actions logic to the existing component, and to add an input which represents the component mode (accounts or requests). This mode will affect the behavior of the component and will display the appropriate actions, accordingly.

However, this solution might have several unwanted consequences at the long run. First, by adding more responsibilities to our code, we are breaking the previous principle (Single Responsibility). Second, we are losing the benefits of angular modules separation and lazy loading. Because now, accounts module and requests module depends on each other. Last, if the way of routing between the accounts actions will be changed in the future, we will have to make changes and to influence the way of requests actions routed, without any real logic.

So we may consider another solution. We can try to change our accounts-actions in a way that it will handle the routing only, and will be open for controlling of the actions, which had to be routed, from outside of this component. In this way, the issues mentioned above were successful solved.

Practically, we can use the angular router this time. It will allow us to declare all the actions components in their related modules, without changing the router logic (which is already closed for changes, by design).

@Component({    selector: 'accounts-actions',    template: `        <action-menu [actions]="getActions() | async">        </action-menu>        <router-outlet></router-outlet>`,    styleUrls: ['./accounts-actions.component.css']})

And we will declare all the routed actions components in the appropriate module. For example, the actions of accounts will be declared in the accounts module:

const routes: Routes = [{    path: 'show',    component: ShowAccountSecretComponent,    runGuardsAndResolvers: 'always' },    { path: 'connect', component: ConnectToAccountMachineComponent,    runGuardsAndResolvers: 'always' },    { path: 'change', component: ChangeAccountSecretComponent,    runGuardsAndResolvers: 'always' }];

Actually, we used this time in the fact that angular developers implement the Open Closed principle in the angular router :-), which is nice, since we don’t need to reinvent the wheel.

This implementation allows us to add new action easily, or change one already exist, without affect the actions router, or other actions in other modules. In addition, now the requests and accounts module are independent, and loaded separately, when needed.

Interface Segregation

Moving along to discuss the actions’ logic. At the original component, all the logic of all the actions were located directly in the component. In this situation, the first instinct, is to take all this logic and move it, as is, to a service. It is easier to test and it make sense to move logic which is not related to the component rendering, from the component file.

I should admit that I had a close enough instinct at a first look, but nevertheless we may benefit from a slight modification. The problem that we still have in this solution, is that now, we need to inject this service with the logic of all the actions, for each action component. In particular, the show-secret-component will get access to change secret logic by the injection, and now, we make it possible, that the coder of the show button, will change the secret first, before showing it.

I believe we should avoid this case. Actually, there is no point in gathering all the logic in the same place. It will be preferable to split it to different services, per action logic, and to inject specific service to the specific related component.

For example, we will create the connect service:

@Injectable()    export class ConnectAccountService {
constructor(private http: HttpClient) {}
……
public connect(accountId: number) {
this.http.get('https://passwordvault/api/Accounts/${accountId}/secret/connect') .subscribe((data: any) => this.downloadRDP(data.file));
}
}

Injecting this specific service to the specific connect component will look as the following:

export class ConnectToAccountMachineComponent implements OnInit,   
OnDestroy {
………
constructor(public connectAction: ConnectAccountService, …) { }
public connectPrerequisitesClick = () => {

this.connectAction.connect();
}
…………}

Dependency Inversion

This principle can most easily be demonstrated if we receive a new requirement, which will extend the original feature. In our case, this new ability named Dual Control (the CyberArk users will not recognize it as I described it, since I simplify the real feature, for the example). The purpose of the ability is to allow users, which have no permissions to do some action, to send a request to the administrator, for allowing make the action. Namely, if your application is configured to support Dual Control, and you don’t have permissions to do “show secret” action, you will have “request show” option in the action menu, and by clicking on it, a full page form will opened instead of dialog. For some users will have a code, which they can insert in a special field in the form, named bypass. If the code is correct, they can get a bridge to make the action in that moment, without sending a request.

A naive solution might be, to create a new component, named CreatRequest, and this component will contain a big switch case, which knowing all the actions supported now in the project, and handling each action appropriately inside this new component.

The problem in this solution is that, we actually added another place in our application, which knows the list of all supported actions, and maybe we also copy much of the code, from the actions logic we already coded in other files.

So a better solution might be to depend on abstraction of action, instead of concrete actions.

We first create the abstract action:

@Injectable()export abstract class AbstractAction {    abstract name: string;    abstract doAction(data: any): any;}

Second, we change the actions services to inherit from this abstract action. For example, the connect service will look as the following:

@Injectable()export class ConnectAccountService extends AbstractAction {    constructor(private http: HttpClient) {        super();    }    public doAction(accountId: number) {        this.http.get('https://passwordvault/api/Accounts/${accountId}/secret/connect')            .subscribe((data: any) =>                this.downloadRDP(data.file));    }
}

Last, the create request service will inherit from the abstract and also, will get by injection, an abstract action, to invoke in case of bypass success:

@Injectable()export class CreateRequestService extends AbstractAction {    constructor(private actionRequested: AbstractAction) {        super();        this.name = 'CreateRequest';    }    doAction(data: any){        if (this.bypass.Succeeded) {            this.actionRequested.doAction(data);        } else {            this.createRequest(this.actionRequested.name, data);        }    }    createRequest(actionName: string, data: any) {        ……
}
}

Liskov Substitution

Changes in requirements is something that we are used to on a regular basis. Sometimes we succeed to finish implementing and test the feature, but then somebody “play” with the new feature, and something is missing.

In our action-menu, as I mentioned earlier, any click on any action, should open a dialog with a relevant content, for execute the action. But now, show and change should have prerequisites. Those prerequisites are different per action, and they should be received dynamically from a web service, per action. The common thing here, is that they need a “getPrerequisites” function. So maybe we will add it to the base service?

If we will add it to the base service, we will enforce the connect service to implement it as well, but connect has no prerequisites. So connect service will now expose function, and implement it, although it does not need to and it does not have this ability.

So we can create hierarchy of inheritance:

@Injectable()export abstract class AbstractAccountActionPrerequisitesService  
extends AbstractAccountsAction {
abstract getPrerequisites(id: number): Prerequisites;}

And we will change only the services which had to use the getPrerequisites function. The other services, will stay without change.

@Injectable()export class ConnectAccountService extends    
AbstractAccountActionPrerequisitesService {

public getPrerequisites(id: number) {
return this.http.get('https://passwordvault/accounts/${id}/retrieve/prerequisites') .pipe(...) } public doAction(data: any) {…}
}

Now, only services which related to actions that need to support prerequisites, will have this function, but all the other services, will not have to change at all.

E. S.O.L.I.D, is a team effort

In this example, I introduced a process of refactoring an existing components and services in according to a design pattern based on the S.O.L.I.D principles. Even though restructuring make for a good example, I believe that in real life those patterns should be implemented at the design stage. When building our design documentation we are in the clearest mind to understand who are all participates in the feature, and the relations between them. It is also the best stage to allow us understanding how we split the feature to tasks according to those separations. At this stage, such patterns can also help us to best allocate tasks in our teams (which should be done in parallel, which by individual or a group etc.).

That not to suggest, that refactoring will never be needed over time. Like all our code maintenance is something we need to consider as time passes. However, refactoring a code that was designed according to the patterns I suggested above is faster, easier and safer.

In those days, when the frontend engineering getting larger and grows, we must be equipped with principles, which allows us to extend our components and maintain them easily and safely.

Shiri Haim

Written by

Web developer. Software Architect at CyberArk. B.Sc Math & CS, HUJI. M.Sc CS candidate, TAU. Exploring design patterns, new technologies and computer algorithms

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade