SOLID: Single Responsibility Principle (SRP) with frontend application

A module should be responsible for one, and just one, actor - Robert Cecil Martin.

SOLID: Single Responsibility Principle (SRP) with frontend application

Intro

In the previous article I commented on Clean Architecture in the frontend. But in clean design we have a set of principles called S.O.L.I.D. Within this acronym we have the SRP (Single Responsibility Principle), which according to the author of the book itself can cause confusion. We will bring in this article important points directed also to the frontend and how we need to be very careful when delegating responsibilities to components within the system when writing them. Let's start with a brief introduction of SRP.

Analogy

We can start this article with an analogy. Imagine you have a restaurant in your town that specializes in typical foods of the country. You love this place and go there every month.

After a few years the establishment is "under new management! ". This causes you some concern and when you go in you realize that they have introduced new dishes to the menu, but still work with the typical foods of the local country. So you ask the waiter for your favorite dish that the cook and his team know how to prepare so well! But when the dish arrives you are surprised by how the meal looks. Ok... what is important is the taste... and when you take your first bite, you are disappointed. The food tastes totally different than it did before. So you decide to talk to the restaurant management and ask to go to the kitchen.

When you enter the kitchen, you understand why this has happened. The chef and his staff are overwhelmed. They have to serve dozens of new dishes with different ingredients. Their specialty was typical dishes from their country, but now they prepare other traditional dishes from other countries! They know how to prepare dishes from other countries and also local dishes. So now there is a lot of responsibility on the same staff, but the staff is not supposed to know all these dishes! This creates dissatisfaction in the customers and unfortunately they cannot serve anyone with excellence!

The problem is that this team was responsible for working with specific dishes, they had the responsibility to serve only customers interested in the dishes of the local country. But now they were also working with dishes from other countries that they are not specialists. Instead of having specialized people to prepare those dishes, to serve interested customers, all the responsibility fell to just one team.

The principle of single responsibility has a lot to do with this. Especially if we consider the kitchen team as classes, components or modules. The new owners of the establishment (actors, company department) were the ones who requested the changes. The manager can be considered the developer who did not know how to separate well the responsibilities with the requirements changes that occurred.

But if the analogy is not very helpful, rest assured. We will cover in the next topics examples in code. Let's understand this principle better.

A single responsibility?

Single Responsibility Principle, described in the book by Robert C. Martin can lead any developer to think that a module, class or component needs to have only one function that performs only one purpose, taking to extremes that instead of helping can hinder.

On one occasion I had the misfortune of participating in a meeting where the project manager questioned a developer about why class X had three declared methods, with an arrogant tone of voice he made a point of explaining that there was a clear violation of SRP because class X should have only one method as described in the Clean Architecture book. Obviously that Manager has not read the book carefully and even less understood what the SRP is about!

The author of the Clean Architecture book himself traces the timeline making it clear that we used to have this description for SRP:

A module must have one, and only one, reason to change

Obviously these changes are requested by someone, which we can call users and stakeholders. The end of the sentence "reason to change" has been adapted for this version:

A module should be accountable to one, and only one, user or stakeholder

But again these are not the best words to use. All the software you create is, in one way or another, made to meet the needs of one person or a group of individuals. When software changes, it is usually to accommodate the changing requirements of these stakeholders. With that in mind we can rephrase the principle as:

A module must be responsible for one, and only one, actor

Modules are basically groupings of functionality that create logical sequences of activities, processing and data storage. The actor is the one who can request the change of that current behavior of the application. In the most popular programming languages we can replace modules with classes.

Can I have a class with more than one actor? It is not recommended to do this as it would violate the SRP, in fact if you notice that your class is serving more than one actor the chances in the future that this will affect the company financially are high! Also, you can have numerous problems with unexpected behavior due to high coupling between components. In the book Robert tells us a story that helps us visualize how having a class that handles more than one actor can hinder the development of future functionality.

We can also have problems on the client side if our components, classes, functions or modules serve more than one user group. Let's understand this better with a code example!

SRP in Code

If classes take on multiple responsibilities, they will be highly coupled, making them more difficult to maintain. With that in mind, take a look at this example:

class Book {
  constructor(private _author: string, private _title: string) {}

  get author(): string {
    return this._author;
  }

  get title(): string {
    return this._title;
  }

  save(): void {
    // persistence book in the database.
  }
}

What is the big problem here? Whenever we want to change persistence, we need to change this class. We have to modify this class. There are several axis of change here. Ideally, you would split Bookclass to have the book representation in one class and the persistence logic in another:

class Book {
  constructor(private _author: string, private _title: string) {}

  get author(): string {
    return this._author;
  }

  get title(): string {
    return this._title;
  }
}

interface RepositoryInterface<T> {
  save(entity: T): void;
}

class BookRepository<T extends Book> implements RepositoryInterface<T> {
  save(book: Book): void {
    // persistence book in the database
  }
}

Look at this other case:

Consider a class for a stock trading application that buys and sells securities. This class, called Transaction, has two responsibilities: buying and selling. In our first example this is done by defining two methods of which the Transaction class will be responsible for defining the behavior.

// Class to handle both Buy and Sell actions
class Transaction {

    // Method to Buy, implemented in Transaction class

    private void Buy (String stock, int quantity, float price) {
        // Buy stock functinality implemented here
    }

    // Method to Sell, implemented in Transaction class

    private void Sell( String stock, int quantity, float price) {
        // Sell stock functionality implemented here
    }
}

This is setting off a warning that something is not right! If the requirements for any of the methods change, this will require a change in the Transaction. In other words, the transaction has multiple responsibilities:

class Transaction {

    // Method to Buy, implemented in Buy class
    private void Buy(String stock, int quantity, float price){
        Buy.execute(stock, quantity, price);
    }

    // Method to Sell, implemented in Sell class
    private void Sell(String stock, int quantity, float price){
        Sell.execute(stock, quantity, price);
    }
}

class Buy {

    // Static method, accessible to other classes, to execute Buy action
    static void execute(String ticker, int quantity, float price){
        // Execute buy action here
    }
}

class Sell {

    // Static method, accessible to other classes, to execute Sell action
    static void execute(String ticker, int quantity, float price){
        // Execute sell action here
    }
}

Now the transaction class is fine doing only its own thing. Each of these refactored classes is then tasked with a single responsibility. The transaction class can still perform two separate tasks, but is no longer responsible for their implementation.

Martin defines "responsibility" as a reason for change. So in the example the class would have a reason to change if we had to adjust how the shares were bought or sold.

With the Buy and Sell actions as separate classes, the only changes to the Transaction class would be necessary for other factors. We might add another action like Logs. Perhaps we would need to add a TransactionTime. The possible extensions are not restricted, but their implementation should maintain the goal of avoiding multiple reasons to change the Transactions class.

Looking at components in the frontend

Development of the frontend in a clear and readable way is a big challenge. Mainly because it is very easy nowadays to overload the components and classes with several responsibilities. But this can become a big problem in a few months when the site or application is much more robust and with many people using it. Developing new functionality required by the stakeholders may be practically impossible. The single responsibility principle tries to help us avoid these problems. Take a look at the code below:

onSubmit() {
        let userlogin = this.insertForm.value;
        this.acct
            .login(userlogin.Username, userlogin.Password, userlogin.RememberMe)
            .subscribe(
                (result) => {
                    this.invalidLogin = false;
                    $('body').css({ 'background-image': 'none' });
                    this.router.navigateByUrl(this.returnUrl);
                },
                (error) => {
                    this.invalidLogin = true;
                    this.ErrorMessage = error;
                    if (error.status == 500) {
                        this.toasterService.info(
                            'Our Team is working to fix this error. Try again later.',
                            '',
                            { positionClass: 'toast-top-right' }
                        );
                    }
                    if (error.status == 401) {
                        if (error.error.loginError) {
                            if (
                                error.error.loginError == 'Auth Code Required'
                            ) {
                                localStorage.setItem(
                                    'codeExpiry',
                                    error.error.expiry
                                );
                                localStorage.setItem(
                                    'twoFactorToken',
                                    error.error.twoFactorToken
                                );
                                localStorage.setItem('isSessionActive', '1');
                                localStorage.setItem(
                                    'user_id',
                                    error.error.userId
                                );
                                this.router.navigate(['/send-code']);
                                return false;
                            }
                            if (error.error.loginError == 'Account Locked') {
                                Swal.fire({
                                    title:
                                        'Your account is locked, please contact support.',
                                    icon: 'error',
                                    showClass: {
                                        popup:
                                            'animate__animated animate__fadeInDown'
                                    },
                                    hideClass: {
                                        popup:
                                            'animate__animated animate__fadeOutUp'
                                    }
                                });
                                return false;
                            }
                            if (
                                error.error.loginError == 'Email Not Confirmed'
                            ) {
                                Swal.fire({
                                    title:
                                        'Your Email is not Verified. Please Verify Email',
                                    icon: 'error',
                                    showClass: {
                                        popup:
                                            'animate__animated animate__fadeInDown'
                                    },
                                    hideClass: {
                                        popup:
                                            'animate__animated animate__fadeOutUp'
                                    }
                                });
                                return false;
                            } else {
                                this.toasterService.error(
                                    error.error.loginError,
                                    '',
                                    { positionClass: 'toast-top-right' }
                                );
                                return false;
                            }
                        } else {
                            this.toasterService.error(
                                'Invalid Username/Password. Please check credentials and try again',
                                '',
                                {
                                    positionClass: 'toast-top-right',
                                    timeOut: 3000
                                }
                            );
                            return false;
                        }
                    } else {
                        this.toasterService.warning(
                            'An error occured while processing this request.',
                            '',
                            { positionClass: 'toast-top-right' }
                        );
                        return false;
                    }
                }
            );
    }

The code above is part of a component that should only send data for a login. Clearly we see that the onSubmitmethod is too polluted! See how there is a cascade of if/else that makes it unreadable. You can see that the function is responsible for manipulating the localStorage. On top of all this CSS arrow $('body').css({ 'background-image': 'none' });.

The solution to this case is to first understand the whole approach of the function and start refactoring it using the recommendations of Martin Fowler in the book Refactoring: Improving the Design of Existing Code. Here is a very interesting quote where he comments exactly about this:

99% of the time, all you have to do to reduce a function is to use the extract function technique. Find parts of the function that seem to fit together and create a new function. - Martin Fowler.

So looking at this dirty code that violates SRP, the developer could separate the repeating conditionals that exist by isolating them in a class or function that would be responsible for checking if the error is 401, 403, 404, 500 and others as needed.

Component that has the responsibility to display the error message and page, we also need HTML, don't forget!:

import { Component, OnInit } from '@angular/core';
@Component({
  selector: 'app-internal-server',
  templateUrl: './internal-server.component.html',
  styleUrls: ['./internal-server.component.css']
})
export class InternalServerComponent implements OnInit {
  errorMessage: string = "500 SERVER ERROR, CONTACT ADMINISTRATOR!!!!";

  constructor() { }
  ngOnInit(): void {
  }
}

Service that takes care of the logical part of redirecting to the error components:

import { HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';

@Injectable({
  providedIn: 'root'
})
export class ErrorHandlerService {
  public errorMessage: string = '';

  constructor(private router: Router) { }

  public handleError = (error: HttpErrorResponse) => {
    if (error.status === 500) {
      this.handle500Error(error);
    }
    else if (error.status === 404) {
      this.handle404Error(error)
    }
    else {
      this.handleOtherError(error);
    }
  }

  private handle500Error = (error: HttpErrorResponse) => {
    this.createErrorMessage(error);
    this.router.navigate(['/500']);
  }

  private handle404Error = (error: HttpErrorResponse) => {
    this.createErrorMessage(error);
    this.router.navigate(['/404']);
  }
  private handleOtherError = (error: HttpErrorResponse) => {
    this.createErrorMessage(error);
  }

  private createErrorMessage = (error: HttpErrorResponse) => {
    this.errorMessage = error.error ? error.error : error.statusText;
  }
}

The code above is just an example of how we can step by step refactor and think better about the responsibilities of each function in our frontend, it is not something that should be done for corporate applications, it is just a small step towards improvement. There are best practices for error throwing within the frontend and we can cover this in another article. Another point to note is that the localStorage could be in a separate class, so if another application component needs to use the localStorage.setItem it would be enough to import the class and inject it into the constructor. Again, this is just a suggestion of how to accomplish this.

Some tips for applying SRP on the frontend are:

  • Understand in depth the real needs of each functionality and its contexts.

  • Do not create generic components unnecessarily!

  • Discuss with the development team in the planning of each task.

Conclusion

Following good practices can help you a lot on a daily basis! SRP is too valuable not to be taken seriously. Understanding how to separate the responsibilities of classes and components within the system is not a simple task, especially when we are working for large corporations in the market! That is why it is important to keep studying and improving every day. As developers it is our duty to defend readable code and clean architectures. Remember that defining well the responsibilities of classes, components and layers can guarantee the failure or success of software.

I'm done with this article and hope that in some way this content has helped you understand the principle of single responsibility. See you next time!

Reference:

Robert Martin Blog

Martin Fowler Blog

Clean Architecture Book

ย