Solid Principle's

Solid Principle's

"Any fool can write code that a computer can understand. Good programmers write code that humans can understand." – Martin Fowler

image.png

Key Concepts

  • What are SOLID principles?
  • Why do we need them?
  • SOLID principles.

What are SOLID principles?

In Java, SOLID principles are an object-oriented approach that is applied to software structure design. It is conceptualized by Robert C. Martin (also known as Uncle Bob). These five principles have changed the world of object-oriented programming, and also changed the way of writing software. It also ensures that the software is modular, easy to understand, debug, and refactor.

Why do we need them?

The SOLID principles were developed to combat tee problematic design patterns. The broad goal of the SOLID principles is to reduce dependencies so that engineers change one area of software without impacting others. Additionally, they’re intended to make designs easier to understand, maintain, and extend.

SOLID principles.

Before diving into the deeps, Lets us understand the acronym of SOLID.

  • S: Single Responsibility Principle (SRP)
  • O: Open closed Principle (OSP)
  • L: Liskov Substitution Principle (LSP)
  • I: Interface Segregation Principle (ISP)
  • D: Dependency Inversion Principle (DIP)

Now let's learn with an example.

Assume we have a movie booking app with all the functionalities like

  1. Show all movies.
  2. Show all theaters.
  3. Show all available seats in a selected theater.
  4. Book the seats.
  5. Make payment(through OTP or Net Banking)
  6. Get e-Tickets.
package com.harsha.tutorials;

class MovieBookingApp{

    //Show all movies
    public List<String> getAllMovies(){
          return new ArrayList<>();  
    }

    //Show all theaters
    public List<String> getAllTheaters(){
        return new ArrayList<>();
    }

    // Show all seats.
    public void showSeatsInTheater(){
        //Show Seats
    }

    // Book Seats
    public void bootSeats(){
        //Block Seats
    }

    // Payments
    public boolean makePayment(){
        //Make Payment
    }

    //e-Tickets
    public void getTickets(){
        if(makePayment()){
            //Publish e-Tickets
        }
        else{
            // Do something
        }
}

SINGLE RESPONSIBILITY PRINCIPLE

This principle states that "a class should have only one reason to change" which means every class should have a single responsibility or single job or single purpose.

In the above code snippet, If the slightest change is required in any of the operations, we gonna change the class.

For example, let us make an assumption of a day where OTP will never receive, only payments are through the net banking, in order to support this change, the whole class is needed to change its implementation.

Here it breaks our very first principle, the Single Responsibility Principle (SRP) as it doesn’t follow the single responsibility principle because this class has too many responsibilities or tasks to perform.

To achieve the goal of the single responsibility principle, we should implement a separate class that performs a single functionality only.

  • For showing all movies.

    class MovieService{
      public void showAllMovies(){}
    }
    
  • For showing all theaters

    class TheaterService{
      public void showAllTheaters(){}
      public void showAllAvailableSeats(){}
    }
    
  • For booking seats

    class BookingService{
      public void blockSeats(Theater theater, List<Integer> seatIds){}
    }
    
  • For payments

    class PaymentService{
      public boolean makePayment(){}
    }
    
  • For e-Tickets

    class PrintingService{
      public void publishETickets(PaymentService payment){}
    }
    

Now each class has a single responsibility.

OPEN CLOSED PRINCIPLE

This principle states that “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification” which means you should be able to extend a class behavior, without modifying it.

Lets us assume that our app has opened for various payment method like UPI, Bitcoin, etc.

It'll violate the OCP If we modify our source code PaymentService for all the implementing all the additional payment methods.

So to overcome this you need to design our code in such a way that everyone can reuse the new feature by just extending it and if they need any customization they can extend it and add their feature on top of it like an abstraction.

We can design like -

public interface PaymentService{
    public boolean makePayment();
}

And we will implement the makePayment() by our means and required mode.

For the UPI mode of payments.

class UPIPayment implements PaymentService {
      public boolean makePayment(){ //using UPI }
}

For the bitcoin mode of payments

class BitcoinPayment implements PaymentService {
      public boolean makePayment(){ //using BITCOIN}
}

This approach would not affect the existing application, and the application can be updated with new features with ease and minimalistic.

LISKOV SUBSTITUTION PRINCIPLE

This principle states that “Derived or child classes must be substitutable for their base or parent classes”. In other words, if class A is a subtype of class B, then we should be able to replace B with A without interrupting the behavior of the program.

This principle is a bit tricky and interesting it is designed based on Inheritance concepts, so let’s better understand this with an example

Let’s consider I have an abstract class called SocialMedia, which supported all social media activity for users to entertain them like below.

public abstract class SocialMedia {
    public abstract  void chatWithFriend();
    public abstract void publishPost(Object post);
    public abstract  void sendPhotosAndVideos();
    public abstract  void groupVideoCall(String... users);
}

Social media can have multiple implantations or can have multiple child-like Facebook, WhatsApp, Instagram, Twitter, etc.

Now let’s assume Facebook wants to use these features or functionalities.

public class Facebook extends SocialMedia {
    public void chatWithFriend() {}
    public void publishPost(Object post) {}
    public void sendPhotosAndVideos() {}
    public void groupVideoCall(String... users) {}
}

Now let’s discuss WhatsApp class.

public class WhatsApp extends SocialMedia {
    public void chatWithFriend() {}
    public void publishPost(Object post) {}
    public void sendPhotosAndVideos() {}
    public void groupVideoCall(String... users) {}
}

Due to publishPost() method in WhatsApp child is not a substitute of parents SocialMedia, because WhatsApp doesn’t support uploading photos and videos for friends it’s just a chatting application.

Similarly, Instagram doesn’t support groupVideoCall() feature so we say the Instagram child is not a substitute of parent SocialMedia.

How to overcome this issue so that our code can follow LSP.

Create a Social media interface.

public interface SocialMedia {  
   public void chatWithFriend();
   public void sendPhotosAndVideos()
}
public interface SocialPostAndMediaManager { 
    public void publishPost(Object post);
}
public interface VideoCallManager{ 
   public void groupVideoCall(String... users);
}

Now it's up to the implementation class decision to support features , based on their desired feature they can use the respective interface, for example, Instagram doesn’t support the video call feature so Instagram implementation can be designed something like this.

public class Instagram implements SocialMedia ,SocialPostAndMediaManager{
  public void chatWithFriend(){}
     public void sendPhotosAndVideos(){}
     public void publishPost(Object post){}
}

INTERFACE SEGREGATION PRINCIPLE

This principle is the first principle that applies to Interfaces instead of classes in SOLID and it is similar to the single responsibility principle. It states that "do not force any client to implement an interface which is irrelevant to them".

Lets us assume we have a UPI-Interface

public interface UPIPayments {   
    public void payMoney();
    public void getScratchCard();
    public void getCashBackAsCreditBalance();
}

As we know that GooglePay, and Paytm do not support CashBackAsCreditBalance as of now. Writing GooglePay.java or Paytm.java that implements UPIPayments the getCashBackAsCreditBalance() will be a forced feature.

We need to segregate the interface based on client needs, so to support this ISP we can design something like the below.

public interface CashbackManager{
 public void getCashBackAsCreditBalance();
}

Now we can remove getCashBackAsCreditBalance from UPIPayments interface .

Based on client needs we segregate interface, let’s say Paytm now implements from UPIPayments then as a client we are not forcing him anything to use.

DEPENDENCY INVERSION PRINCIPLE

The principle states that we must use abstraction (abstract classes and interfaces) instead of concrete implementations. High-level modules should not depend on the low-level module but both should depend on the abstraction. Because the abstraction does not depend on detail but the detail depends on abstraction. It decouples the software.

Let us assume that we have two payment methods for shopping Debit and Credit cards. By default, the user would like to proceed with a Debit card.

Debit Card

public class DebitCard{
  public void doTransaction(int amount){}
}

Credit Card

public class CreditCard{
  public void doTransaction(int amount){}
}

Shopping Mall

public class ShoppingMall {
    private DebitCard debitCard;
    public ShoppingMall(DebitCard debitCard) {
          this.debitCard = debitCard;
     }
    public void doPayment(Object order, int amount){                      
        debitCard.doTransaction(amount); 
     }
}

But during the payment at a shopping mall, we couldn't proceed further because if something was wrong with our debit card the whole payment is disrupted. This is because of the tight coupling between the ShoppingMall and DebitCard.

We could write the code to follow DIP like below.

public interface BankCard {
  public void doTransaction(int amount);
}

Now both DebitCard and CreditCard will use This BankCard as an abstraction.

Debit Card

public class DebitCard implements BankCard{
    public void doTransaction(int amount){}
}

Credit Card

public class CreditCard implements BankCard{
    public void doTransaction(int amount){}
}

Now for shopping mall.

public class ShoppingMall {
    private BankCard bankCard;
    public ShoppingMall(BankCard bankCard) {
          this.bankCard = bankCard;
     }
    public void doPayment(Object order, int amount){
          bankCard.doTransaction(amount);
    }
}

Now if you observe shopping mall is loosely coupled with BankCard, any type of card processes the payment without any impact.

And this is all about our beloved SOLID principles.

Next Article: CRUD using Spring Boot & MongoDB.

See Ya.!!