XenonStack Recommends

Data Visualization

SOLID Principles in JavaScript

Navdeep Singh Gill | 22 August 2024

SOLID Designs Principles in JavaScript and TypeScript

What is SOLID in TS/JS?

A few terms to keep in mind before starting SOLID are the principle of least knowledge, coupling, and cohesion.

Principle of least knowledge

Each unit (class, function, object, module) should have only limited knowledge about other units. This helps in the reusability maintainability of code.

Survey shows that design is the most important of web and mobile users’ positive first impressions. Click to explore about, UX UI Designs and Latest Trends

What is Low Coupling and High Cohesion?

  • Coupling: The degree of direct knowledge that one element has of another is called coupling.
  • Cohesion: Within a module, this measures how closely connected components are. It's a metric for how closely things resemble one another. Do you have a lot of stuff in here that's mainly about the topic at hand? Is there anything in here that belongs somewhere else?

Our software is composed of a bunch of unit classes or objects. An object encapsulates data and relevant methods related to data. Objects should be loosely coupled, i.e., they should be less dependent on each other. Each object should know less about another object. The same goes for different features or modules. S.O.L.I.D Principles help to achieve it.

  • Feature: a collection of objects to perform a specific task is called feature (package).

What are the S.O.L.I.D Principles and why do we need them?

These are a set of software design principles that instruct us on how to structure our functions and classes to make them as reliable, maintainable, and adaptable as feasible.

If the code you've built in the past doesn't meet your current needs, changing it can be costly. We want to jot down any code that will be altered. Changing your code a second, third, and fourth time should not add defects or make it difficult to scale your previous code.

SOLID stands for

  • S: Single Responsibility Principle
  • O: Open-Closed Principle
  • L: Liskov-Substitution Principle
  • I: Interface Segregation Principle
  • D: Dependency Inversion Principle

The SOLID is explained below:

CSS is near revolutionizing the concept of responsive design by adding some new queries. Click to explore about, New Component-Driven Responsive Design

S: Single Responsibility Principle

A class or function should have one and the only reason to change. Each class should do one thing & do it well. Instead of thinking that we should split code because it would look cleaner in a single file, we split code up based on the users' social structure. Because that's what dictates change. Few things to note:

  • Don't put functions in the same class that change for various causes.
  • Think responsibilities (reason to change) regarding the user who will use it.
  • The class should be low coupling & high cohesive.

if we had a Technical department and a Finance department in an enterprise application that calculates salary, working hours, and saves records to DB.We'd better make sure we've split up (or abstracted) the operations most likely to change for each department in an enterprise application that calculates salary, working hours, and saves records to DB

 

class Employee {
public calculateSalary (): number { // code...}
public hoursWorked (): number { // code..}
public storeToDB (): any { // code.. }
} //Here's an SRP violation:

All functions have the same logic, changing a function for one department will affect other departments too. If the head of Finance changes the logic of their department, it will affect the technical department, or if we try to handle it in the same class, it leads to a bad nested if-else or switch statement.

abstract class Employee {
abstract calculateSalary (): number;
abstract hoursWorked (): number;
protected storeToDB ():any {
}
}
// we are forcing the developer to write their own implementation for different class by abstract methods
class Technical extends Employee {
calculateSalary (): number { ….code }
hoursWorked (): number {...code }
}
class Finance extends Employee {
calculateSalary (): number {...code}
hoursWorked (): number {..code}
}

Still confused about what should go inside a class: start thinking in terms of who is going to use it (roles & user).

O: Open-Closed Principle

Software entities (classes, modules, functions, and so on) should be extensible but not modifiable ( no change in old code). The above approach is based on the premise that we should be able to introduce new features without changing the present code.

Violation Open Closed Principle: a bunch of if or switch statements.

Why should we avoid a bunch of if or nesting or switch statements?

Multiple Execution flow will lead to more bugs.

UX is the most important building block in software development. Click to explore about, User Experience in Software Product Development

L: Liskov Substitution Principle

Objects of a superclass should be able to be replaced with objects of subclasses without causing the application to break. Rectangular Square problem. Every single place where you use Rectangle(parent class) should be replaced with Square(child class) -

we cannot we do that because

All squares are rectangle but vice versa is not true and Liskov's principle fails

Class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}

setWidth(width) {
this.width = width
}

setHeight(height) {
this.height = height;
}

area ( ) {
return this.width * this.height
}

}
Class Square extends Rectangle {

setWidth(width) {
this.width = width
this.height = height;
}

setHeight(height) {
this.height = height;
}

}


const rectangle = new Rectangle(10, 2)
const square = new Square(5, 5)


function increaseRectangleWidth(rectangle) {
rectangle.setWidth(rectangle.width +1)
}
increaseRectangleWidth(rectangle) // 22
increaseRectangleWidth(square) //36

Solution

Take a new class Shape as a parent class and rectangle, the square should inherit(extend from that Object. i.e., Making parent class more generic

Class Shape {
area(): void { }
}

You can’t do multiple inheritances, which may create a lot of generic parent classes, so we go for composition over inheritance.

Data Visualization with JavaScript permits the information tables to view out and not be so dull, so one should strongly justify taking advantage of it. Click to explore about, Data Visualization JavaScript Libraries

I: Interface segregation

"Clients should not be pushed to employ interfaces that they are unfamiliar with or they don't want to use"

This approach aims to reduce the negative consequences of using large interfaces by breaking them down into smaller ones. It's similar to the Single Responsibility Principle, which asserts that any class or interface should be used for only one purpose.

Note: JS does not have an interface

Clients should not be exposed to methods that they do not require (design a tiny interface that does not force any class or function to use an interface they do not wish to use).

Angular Example

export class SummaryPageComponent implements OnInit {
OnInit is an Interface
}

export interface OnInit { //small interface
ngOnInit(): void; //only one method
}

Problem with big interface

export class SummaryPageComponent implements LifeCycles {
//you will be exposed to all classes even if they don't need them
}

export interface LifeCycles {
//too many methods
ngOnInit(): void;
ngOnChanges(changes: SimpleChanges): void;
ngDoCheck(): void;
ngOnDestroy(): void;
}

Java vs Kotlin
Our solutions cater to diverse industries with a focus on serving ever-changing marketing needs. Click here for our Digital Product Development

D: Dependency Inversion Principle

Dependency: When one class is used inside another. As a result, our class is reliant on another. Your code should be based on abstraction rather than implementation.

Low-level modules should not be relied upon by high-level modules. Abstractions should be used in both cases.

class GooglePayService {
constructor (googlePayInstance) {
this.gps = googlePayInstance;
}
pay (to, amount ) {
}
}

10 months later, if your manager tells they want to change payment service to PhonePay instead of GooglePay or use both, your code should be scalable for extension

type PaymentTransaction = 'Success' | 'Failure' | 'Bounced'

interface IPaymentTransactionResult {
result: PaymentTransaction;
message?: string;
}

interface IPaymentService {
pay(to: string, amount: number): Promise<IPaymentTransactionResult>
}


class GooglePayService implements IPaymentService {
pay(to: string, amount: number): Promise<IPaymentTransactionResult> {
// algorithm
}
}

class PhonePayService implements IPaymentService {
pay(to: string, amount: number): Promise<IPaymentTransactionResult> {
// algorithm
}
}

Then we can "Dependency Inject" it into our classes, referencing the interface rather than one of the concrete implementations.

class CreateUserController extends BaseController {
constructor (paymentService: IPaymentService) {
this.paymentService = paymentService;
}

protected proceedPayTransaction (): void {
// api handling...

// send amount
this.paymentService.pay(upiId, amount);
}
}

Now, you can pay using any payment methods you like, and you can even add more payment methods.

const phonePayService = new PhonePayService();
const createUserController = new CreateUserController(phonePayService);
createUserController.proceedPayTransaction();

const googlePayService = new GooglePayService();
const createUserController = new CreateUserController(googlePayService);

Conclusion

Implementing S.O.L.I.D principles in your code will help in the Code reusability, Adapt to new code or easy code changes in future, Easy to read, Easy to maintain, and Easy to test.