DEV Community

Shravan Kumar B
Shravan Kumar B

Posted on • Edited on • Originally published at ohmyscript.com

SOLID Principles: Write SOLID programs; Avoid STUPID programs

Alt Image
“Think Twice, Code Once”

Hi everyone! This is a revised version of my article from my personal blog.

Previously, in my last article, I had explained some of the must-know fundamental programming principles, which are applicable in any programming paradigm that you follow. Be it Functional or Object-Oriented paradigm/programming, those serve as the primary fundamentals.

This article purely speaks of another 5 design principles, most specifically hold good to problems that can be solved using OOPs paradigm.

With the rise of OOPs paradigm, brought new designs and techniques of writing the solution to a problem.

Similarly, on a larger scale, this technique caused some flaws in the solution we design and write, which often we fail to recognize the bugs added in the form of STUPID code.

As I started programming in Typescript standards, implementing OOPS, had become easier, better, smaller and cleaner. I realised one thing after moving from Functional Paradigm to OOPs paradigm, that knowingly or unknowingly we end up implementing some sort of anti-patterns into our codebase.

What’s a STUPID codebase?

A STUPID codebase is that codebase which has flaws or faults, which affect the maintainability, readability or efficiency.

Anti-Pattern Code == STUPID Code

What causes STUPID codebase?

AltImage
Why be STUPID, when you can be SOLID

  • Singleton: Violation of Singleton basically decreases the flexibility and reusability of the existing code, which deals with the object creation mechanism. It is an anti-pattern, where we define a class and its object in the same script/file and export the object for reusability. This is pattern is not wrong, but using it everywhere inappropriately is an symptom sick codebase.
/**
*
*  Creating class Singleton, which is an Anti Pattern 
*  definition.
* 
*  WHY?
*  Let us see.
*/
class Singleton {
  private static instance: Singleton;
  private _value: number;

  /**
  * To avoid creating objects directly using 'new' 
  * operator
  * 
  * Therefore, the constructor is accessible to class 
  * methods only
  */
  private constructor() { } 

  /**
  * Defining a Static function, so to directly
  *  make it accessible without creating an Object
  */
  static makeInstance() {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
      Singleton.instance._value = 0;
    }
    return Singleton.instance;
  }

  getValue (): number {
    return this._value;
  }

  setValue(score) {
    this._value = score;
  }
  incrementValueByOne(): number {
    return this._value += 1;
  }
}


/**
*  Since the Singleton class's constructor is private, we  
*  need to create an instance using the static method 
*  makeInstance()
*  
*  Let us see what anomalies does that cause.
*    
*  Creating an instance using 'new' throw an Error
*  Constructor of class 'Singleton' is private and 
*  only accessible within the class declaration
*  const myInstance = new Singleton(); 
*/

const myInstance1 = Singleton.makeInstance();
const myInstance2 = Singleton.makeInstance();

console.log(myInstance1.getValue()); // OUTPUT: 0
console.log(myInstance2.getValue()); // OUTPUT: 0


myInstance1.incrementValueByOne(); // value = 1
myInstance2.incrementValueByOne(); // value = 2

console.log(myInstance1.getValue()); // OUTPUT: 2
console.log(myInstance2.getValue()); // OUTPUT: 2

/**
* This is the issue Singleton Anti-Pattern
* causing Issue with Singleton Pattern
*/
Enter fullscreen mode Exit fullscreen mode
  • Tight-Coupling: Excessive coupling/dependency between classes or different separate functionality is a code smell, we need to be very careful about while we are developing or programming. We can figure tight-coupling when a method accesses the data of another object more than its own data or some sort of functional chaining scenarios.
/**
* A simple example for Tight-Coupling
*/

class Car {

  move() {
    console.log("Car is Moving");
  }

}

class Lorry {

   move(){
      console.log("Lorry is Moving");
   }

}

class Traveller1 {

  Car CarObj = new Car();

  travellerStatus(){
     CarObj.move();
  }    

}

class Traveller2 {

  Lorry LorryObj = new Lorry();

  travellerStatus(){
     CarObj.move();
  }    

}
Enter fullscreen mode Exit fullscreen mode
  • Untestabiility: Unit Testing is a very important part of software development where you cross-check and test if the component you built is functioning exactly the way expected. It is always advised to ship a product only after writing test cases. Shipping an untested code/product is very much similar to deploying an application whose behaviour you are not sure about.
    Apart from Unit testing, we have other tests like Integration testing, E2E testing and so on, which are done based on their use cases and necessity.

  • Premature Optimizations: Avoid refactoring code if it doesn’t improve readability or performance of the system for no reason.
    Premature optimisation can also be defined as trying to optimizing the code, expecting it to improvise the performance or readability without having much data assuring it and purely weighing upon intuitions.

  • Indescriptive Naming: Descriptive Naming and Naming Conventions are two important criteria. Most of the times, naming becomes the most painful issue.
    After some time when you or another developer visits the codebase, you would be asking the question ‘What does this variable do?’. We fail to decide what would be the best descriptive name that can be given to a variable, class, class object/instance or function. It is very important to give a descriptive name, for better readability and understandability.

/**
* Example for adding two numbers: Avoid this
*/
function a(a1,a2) { // It is less descriptive in nature
  return a1 + a2;
}

console.log(a(1,2)); // It is less descriptive in nature


/**
* Example for adding two numbers: Better Approach
*/
function sum(num1,num2) {  // sum() is descriptive
  return num1 + num2;
}

console.log(sum(1,2)); 
// Statement is descriptive in nature
Enter fullscreen mode Exit fullscreen mode
  • Duplication: Sometimes, duplication of code is resultant of copy and paste. Violation of DRY principle causes code-duplication. Always advised not to replicate the code across the codebase, as on longer run causes huge technical debt. Duplication makes code maintenance tedious on a larger scale and longer run.

These flaws were often overlooked knowingly or unknowingly, for which SOLID principles served as the best cure.

So, you wondering now what SOLID principles hold and how does it solve the issues caused due to STUPID postulates. These are programming standards that all developers must understand very well, to create a product/system with good architecture.
SOLID principles can be considered as remedies to the problems caused due to any of the STUPID flaws in your codebase.
Uncle Bob, otherwise known as Robert C Martin, was the Software Engineer and Consultant, who came up with mnemonic acronym SOLID in his book “Clean Coder”. Let’s explore a little more on SOLID principles in detail,

Single Responsibility Principle (SRP)

A class, method or function should undertake the responsibility of one functionality. In simpler words, it should carry out only one feature/functionality.

A class should only have a single responsibility, that is, only changes to one part of the software’s specification should be able to affect the specification of the class.
-Wikipedia

In OOPs paradigm, one class should only serve one purpose. This does not mean that each class should have just one method, but the methods you define inside a class should be related to the responsibility of that class.

Let us look into it using a very basic example,

/**
* Here, Class User bundled with functionalities which
* deals with business logic and DB calls defined 
* in the same class
*    
* STUPID Approach
*/

class User {

constructor() {...}

/**
* These methods deal with some business logic
*/

//Add New User
public addUser(userData:IUser):IUser {...}

//Get User Details Based on userID
public getUser(userId:number):IUser {...}

//Get all user details
public fetchAllUsers():Array<IUser> {...} 

//Delete User Based on userID
public removeUser(userId:number):IUser {...}


/**
* These methods deal with Database Calls
*/

//Save User Data in DB
public save(userData:IUser):IUser {...}

//Fetch User Data based on ID
public find(query:any):IUser {...}

//Delete User Based on query
public delete(query:any):IUser {...}

}
Enter fullscreen mode Exit fullscreen mode

The problem in the above implementation is that, methods that deals with business logic and related to database calls are coupled together in same class, which violates the Single Responsible Principle.

The same code can be written ensuring the SRP is not violated, by dividing the responsibilities for dealing business logic and database calls separately, as shown in the below instance

/**
*  We will apply the SOLID approach for the 
*  previous example and divide the responsibility. 
*
* 'S'OLID Approach  
*/

/**
* Class UserService deals with the business logic 
* related to User flow
*/

class UserService {

constructor() {...}

/**
* These methods deal with some business logic
*/

//Add New User
public addUser(userData:IUser):IUser {...}

//Get User Details Based on userID
public getUser(userId:number):IUser {...}

//Get all user details
public fetchAllUsers():Array<IUser> {...} 

//Delete User Based on userID
public removeUser(userId:number):IUser {...}
}


/**
* Class UserRepo deals with the Database Queries/Calls
* of the User flow
*/
class UserRepo {

constructor() {...}

/**
* These methods deal with database queries
*/

//Save User Data in DB
public save(userData:IUser):IUser {...}

//Fetch User Data based on ID
public find(query:any):IUser {...}

//Delete User Based on query
public delete(query:any):IUser {...}

}
Enter fullscreen mode Exit fullscreen mode

Here, we are ensuring a specific class solves a specific problem; UserService dealing with business logic and UserRepo dealing with database queries/calls.

Open-Closed Principle (OCP)

This principle speaks about the flexibility nature of the code you write. As the name stands for itself, the principle states that the solution/code you write should always be Open for extensions but Closed for modifications.

Software entities … should be open for extension, but closed for modification.
-Wikipedia

To put it up in simpler words, code/program you write for a problem statement, be it a class, methods or functions, should be designed in such that, to change their behaviour, it is not necessary to change their source code/reprogram.

If you get additional functionality, we need to add that additional functionality without changing/reprogramming the existing source code.


/**
* Simple  Notification System Class Example for 
* violating OCP
*
* STUPID Approach of Programming
*
*/

class NotificationSystem {

 // Method used to send notification
  sendNotification = (content:any,user:any,notificationType:any):void => {

    if( notificationType == "email" ){
      sendMail(content,user); 
    }

    if( notificationType == "pushNotification" ){
      sendPushNotification(content,user); 
    }

    if( notificationType == "desktopNotification"  ){
      sendDesktopNotification(content,user); 
    }

  }

}

Enter fullscreen mode Exit fullscreen mode

The major setback with the above approach is that again if a newer way of sending a notification or combined notifying mechanism is needed, then we need to alter the definition of the sendNotification().

This can implemented ensuring the SOLID principle not being violated, as shown below,

/**
* Simple Example for Notification System Class  
*
* S'O'LID Approach of Programming
*
*/

class NotificationSystem {

    sendMobileNotification() {...}

    sendDesktopNotification() {...} 

    sendEmail() {...}

    sendEmailwithMobileNotification() {
      this.sendEmail();
      this.sendMobileNotification()
    }
}
Enter fullscreen mode Exit fullscreen mode

As you see in the above example, when you needed another requirement where you had to send both email and mobile notification, all I did was adding another function sendEmailwithMobileNotification() without changing the implementation of previous existing functions. That’s how simple it is, making an extension of features.

Now, moving on to next important principle, called as Liskov Substitution principle.

Liskov Substitution Principle (LSP)

This principle is the trickiest one. Liskov Substitution Principle was introduced by Barbara Liskov in her paper called “Data Abstraction”.
By now, you already must have known that this principle has to do with the way we implement Abstraction.

Recalling, what is abstraction/data abstraction? In simplest words, hiding certain details and showing essential features.
Example: Water is composed of Hydrogen and Oxygen, but we see is a liquid matter (Abstraction)

“Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.”
-Wikipedia

According to LSP in the OOP paradigm, child classes should never break the parent class type definition.
To put it in even simpler bits, all subclass/derived class should be substitutable for their base/parent class. If you use base type, you should be able to use subtypes without breaking anything.

altIMAGE

/**
* Simple hypothetical example that violates  
* Liskov Principle with real-time situation
*
* STUPID Approach
*/

class Car {
  constructor(){...}

  public getEngine():IEngine {...}  
  public startEngine():void {...}
  public move():void {...}
  public stopEngine():IEngine {...}
}
/* 
* We are extending class Car to class Cycle
*/
class Cycle extends Car {  
    constuctor(){...}
    public startCycle() {...}
    public stopCycle() {...}  
}
/**
* Since Cycle extends Car; 
* startEngine(), stopEngine() methods are also
* available which is incorrect and inaccurate abstraction
*
* How can we fix it?
*/
Enter fullscreen mode Exit fullscreen mode

What we can draw from the LSP violation, causes tight coupling and less flexibility to handle changed requirements. Also, one thing that we take away from the above example and principle is that OOP is not only about mapping real-world problems to objects; it is about creating abstractions.

/**
* Simple hypothetical example that follows the 
* Liskov Principle with real-time situation
*
* SO'L'ID approach
*/

class Vehicle {
  constructor(){...}

  public move():void {...}
}

class Car extends Vehicle {
  constructor(){...}

  public getEngine():IEngine {...}  
  public startEngine():void {...}
  public move():void {...}
  public stopEngine():IEngine {...}

}

/* 
* We are extending class Car to class Cycle
*/
class Cycle extends Car {  
    constructor(){...}

    public startCycle() {...}
    public move() {...}   
    public stopCycle() {...}  
}
/**
* Since class Cycle extends Vehicle; 
* move() method is only also available and applicable
* which is precise level of abstraction
*/
Enter fullscreen mode Exit fullscreen mode

Interface Segregation Principle (ISP)

This principle deals with the demerits and issues caused when implementing big interfaces.

“Many client-specific interfaces are better than one general-purpose interface.”
-Wikipedia

It states that we should break our interfaces into granular small ones so that they better satisfy the requirements. This is necessary so as to reduce the amount of unused code.

/**
*  Simplest Example that violates Interface 
*  Segregation Principle 
*
*  STUPID Approach
*
*  Interface for Shop that sells dress and shoes 
*/

interface ICommodity {
   public updateRate();
   public updateDiscount();

   public addCommodity();
   public deleteCommodity();

   public updateDressColor();
   public updateDressSize();

   public updateSoleType();

}
Enter fullscreen mode Exit fullscreen mode

Here we see that, one interface ICommodity is created for the items/commodity in shop; which is incorrect.

/**
*  Simplest Example that supports Interface 
*  Segregation Principle 
*
*  SOL'I'D Approach
*
*  Separate Interfaces for Shop that sells dress and shoes 
*/

interface ICommodity {
   public updateRate();
   public updateDiscount();
   public addCommodity();
   public deleteCommodity();
}


interface IDress {
   public updateDressColor();
   public updateDressSize();
}

interface IShoe {
   public updateSoleType();
   public updateShoeSize();
}
Enter fullscreen mode Exit fullscreen mode

This principle focuses on dividing the set of actions into smaller parts such that Class executes what is required.

  • Dependency Inversion Principle (DIP)

This principle states that we should depend upon abstractions. Abstractions should not be dependent on the implementation. The implementation of our functionality should be dependent on our abstractions.

One should “depend upon abstractions, [not] concretions.”
-Wikipedia

Dependency Injection is very much correlated to another term called as Inversion of Control. These two terminologies are can be explained differently in two situations.

  1. Based on Framework
  2. Based on Non-Framework ( Generalistic )

Based on programming in Framework, Dependency Injection is an application of IoC, i.e., Inversion of Control. Technically speaking, Inversion of Control is the programming principle, that says invert the control of the program flow.

To put it up in simpler words, the control of a program is inverted, i.e., instead of programmer controlling the flow of the program. IoC is inbuilt with the framework and is a factor that differentiates a framework and library. Spring Boot is the best example.

altimagee Voila! Spring Boot developers! Inversion of Control made sense!! Didn’t it?

Note: For all Spring Boot developers, just like how annotation take control over your program flow

Based on the general perspective, we can define IoC as the principle that ensures, “An object does not create other objects on which they rely to do their work”.
Similarly, based on the general perspective, DIP is a subset principle to IoC, that states define interfaces to make it easy to pass in the implementations.

/**
* Simple Example for DIP
*
* STUPID Approach
*/

class Logger {
   debug(){...}

   info(){...}
}

class User {
  public log: Logger;

  constructor(private log: Logger){...} // =>Mentioning Type Logger Class

  someBusinessLogic(){...} //uses that this.log
}


/**
* Simple Example for DIP
*
* SOLI'D' Approach
*/

interface ILogger {
  debug();
  info();
  error();
}

class Logger implements ILogger{
   debug(){...}

   info(){...}
}

class User {
 public log: ILogger;

 constructor(private log: ILogger){...}
        //=>Mentioning Type Logger Interface

  someBusinessLogic(){...} //uses that this.log
}
Enter fullscreen mode Exit fullscreen mode

If you look into the above examples, the Object creation is dependent on the interface and not on the class.

These are the OOPs Paradigm Programming Principle that makes your code more readable, maintainable and clean.

As a developer, we should avoid trying to write dirty or STUPID code. These are the basic things, we need to keep in mind during the development.

SOLID is no panacea or remedy for all the problems. Some problems in Computer Science can be solved using basic engineering techniques. SOLID is one such technique that helps us maintain healthy codebase and clean software. The benefits of these principles are not immediately apparent but they become noticed and visible over time and during the maintenance phase of the software.

As a developer, it is my suggestion that every time you design or program a solution, ask yourself “Am I violating the SOLID principles?”, if your answer is YES, too long, then you should know that you are doing it wrong.
One thing that I can assure is, these principles are always going to help us write better code.


If you like the article, hit the like button, share the article and subscribe to the blog. If you want me to write an article on specific domain/technology I am provisioned in, feel free to drop a mail at shravan@ohmyscript.com

Stay tuned for my next article.

That’s all for now. Thank you for reading.

Signing off until next time.
Happy Learning.

Top comments (19)

Collapse
 
adam_cyclones profile image
Adam Crockett 🌀

OOP and SOLID is not for everything.

Collapse
 
shravan20 profile image
Shravan Kumar B

Just a fact that, SOLID makes you a better developer. Something you learn with experience.

Collapse
 
adam_cyclones profile image
Adam Crockett 🌀

Some of the solid principles certainly do apply to FP and OPP that is true. But when I was a junior I used to blindly quote SOLID for everything, not a mistake I wish others to repeat.

Thread Thread
 
shravan20 profile image
Shravan Kumar B

Yes. Admit what you are saying.

Collapse
 
jwp profile image
John Peters • Edited

Good OOP winds up as functional parts anyway. Why? The Single Responsibility Principle. Ultimately functional programming styles and OOP are the same thing when favoring composition over inheritance.
The whole Inheritance discussion is of much lesser importance; but unfortunately, many articles start off with discussing it.
Traditional inheritance is an advanced topic and not strictly needed or taught in Javascript. This is due to the excellent 'functions as true first class citizens' feature of Javascript.

C# and Java don't treat functions as true first class functions which leads to other solutions. All of which we are too familiar.

Collapse
 
shravan20 profile image
Shravan Kumar B

Certain problem statements can fit perfectly into design patterns. Design Patterns where entity are considered as objects, are easily resolvable. FP is a great way of solving problems, but not serves all the purpose.

Thread Thread
 
jwp profile image
John Peters

Not true. How does a method or function not solve a problem?

Thread Thread
 
shravan20 profile image
Shravan Kumar B

Programming is not confined to web-development alone.

  • Real-time embedded programming is all about the side effects. Interacting with digital and analog io, timers, serial and parallel ports, everything interesting is done by calling functions with side efffects.

  • GUI programming is not a good fit for functional programming

  • Games; they are easier to manage stateful.

Major difference in FP and OPP:

  • OPP is stateful to an extent
  • FP asks to pure; without any side effect, i.e., stateless

As I said, FP is great, not suitable for all problem statements.

Thread Thread
 
jwp profile image
John Peters • Edited

Wrong. Every program ever, uses functions or what is called a method in OOP. This is the start of a functional style. There are different definitions of functional styles. We are free to mix and match any of them. This includes pure and non pure, mutatable and non mutatable functions. It includes async and non-async, it includes reactive and non reactive.

Correctly written functions have just one concern and optional mutation. Purity is not mandatory in functional programming.

Functions openly expose parameters which is the interface, which requires an implicit required state to enter. They can advance the state either by mutation or return a non mutated result.

All state is the responsibility of a parent container which composes functions. This, 'controller' is perfect for GUI and any other application merely for following compositional styles.

C# and Java do not have true first class functions, unlike Typescript and Javascript . This makes functional styles more difficult but totally doable in Java and C#, via class based functions or complex classes containing other classes or even static function calls.

So the issue ultimately is a non issue when we choose to really understand and use compositional technique as was recommended in 1985 via the GoF book. 'Favor composition over Inheritance'

When we do this correctly we arrive at the door step of Functional styles. We are free to enter in and dive as deep as we want. Single Responsibility takes us deeper.

In the end there's no difference in Compositional OOP and Functional programming except in articles that like to focus on non compositional polymorphic design versus pure functional styles. It's really a waste of time doing that because there's no similarity nor was there ever intended to be similarity.

The only reason this keeps getting airplay is due to Javascript people disliking any discussion of OOP. They don't even like SOLID and rarely even follow DRY. They don't even like the Class object or the New keywords which are part of their own language.

Thread Thread
 
fkrasnowski profile image
Franciszek Krasnowski

Good point, asserting that functional style is stateless is harmful and misleading. Functional approach favors immutability, purity, composition, and declarative style. It doesn't mean it’s hard or disallowed to introduce the state. It’s just brought to a minimum. GUI is not a good fit for fp? So what about React which favors immutability? Or Rust as embedded lang?

Collapse
 
evrtrabajo profile image
Emmanuel Valverde Ramos

Nice article, a few weeks ago I've posted mine talking about SOLID in PHP with real examples, I hope it may help you. dev.to/evrtrabajo/solid-in-php-d8e

Collapse
 
shravan20 profile image
Shravan Kumar B

Thank you.

Collapse
 
jwp profile image
John Peters • Edited

In the United States, a cycle is not ever a car.

Therefore, this is a bad example for the United States, because it violates inheritance rule #1, do not ever violate 'is-a' relationships.

Collapse
 
ilumin profile image
Lumin

Always found Premature Optimizations and Premature Implementation 😢

Collapse
 
shravan20 profile image
Shravan Kumar B

Did not get you

Collapse
 
ilumin profile image
Lumin

Sorry for my bad English, I'm not so good at it please be patien with me 😢

I mean, I've always found these anti pattern in my project

  • Premature Optimizations ~ like you said, reafctor for no reason
  • Premature Implementation ~ implement code to support future features
Thread Thread
 
shravan20 profile image
Shravan Kumar B

Hey, no issues.

Premature Implementation and Optimisation is something we learn to avoid with experience.

Collapse
 
dualyticalchemy profile image
⚫️ nothingness negates itself

functional programming achieves SOLD more easily and with less side- effects than OOP. i'm sold!!

Collapse
 
shravan20 profile image
Shravan Kumar B • Edited

Functional Programming definitely has its perks over OOPs in context with SOLID principles.