In this post, we will understand SOLID principles and why they are important. First of all, what is SOLID?

S -> Single-Responsiblity Principle

O -> Open-Closed Principle

L -> Liskov Substitution Principle

I -> Interface Segregation Principle

D-> Dependency Inversion Principle

We should be writing code that help us develop applications that are robust, error free and scalable. SOLID principles help us develop such applications.

  1. Single-Responsibility Principle ( SRP )
    This principle states that a class should do only one task and should not represent multiple responsibilities. Here is an example:

    interface Shape{
        int area();
        int perimeter();
    }
    
    class Rectangle implements Shape{
        public int area(){
        }
        public int perimeter(){
        }
    }
    
    class Square implements Shape{
        public int area(){
        }
        public int perimeter(){
        }
    }
    
    class ShapeTransaction{
        public void save(Shape shape){
        }
        public List<Shape> load(){
        }
    }

    In the above example, saving and loading of shapes will be handled by ShapeTransaction class. The logic for saving and loading has been separated from individual classes and they can focus on their own logic.

  2. Open-Closed Principle ( OCP )
    This principle states that an class should be open for extension but closed for modification. Hence, to alter a logic, we should not modify the original class. Instead, we should create a new class and extend it from existing class. Here is an example:

    class Square{
    }
    
    class Calculator{
        int totalArea(Square[] squares){
        }
    
        int totalPerimeter(Square[] squares){
        }
    }

    The problem with the above Calculator is that if we wish to calculate area and perimeter for Square as well as Rectangle, we need to modify this class. This is against OCP. To make it OCP complaint, we will make following changes:

    interface Shape{
        int area();
    }
    
    class Rectangle implements Shape{
        public int area(){
        }
    }
    
    class Square implements Shape{
        public int area(){
        }
    }
    
    class Calculator{
        int totalArea(Shape[] shapes){
            int sum = 0;
            for(Shape shape : shapes)
               sum += shape.area();
    
            return sum;
        }
    }

    Now, the Calculator class is closed for modification but it is extensible and we can add any number of shapes as we want.

  3. Liskov Substitution Principle ( LSP )
    This principle works with what we call Runtime Polymorphism. This principle states that we can replace base class objects by the objects of subclasses without changing the current functionality of the program. Here is an example:

    class Square{
        int area(){
        }
    }
    
    class Rectangle{
        int area(){
        }
    }
    class Test{
        void show(){
             Square s;
             Rectangle r;
    
             s = new Square();
             int areaOfSquare = s.area();
    
             r = new Rectangle();
             int areaOfRectangle = shape.area();
        } 
    }

    Now, to make it LSP complaint, we will update the code as follows:

    interface Shape{
        int area();
    }
    
    class Rectangle implements Shape{
        int area(){
        }
    }
    
    class Square implements Shape{
        int area(){
        }
    }
    
    class Test{
        void show(){
             Shape shape;
    
             shape = new Square();
             int areaOfSquare = s.area();
    
             shape = new Rectangle();
             int areaOfRectangle = shape.area();
        }
    }
  4. Interface Segregation Principle ( ISP )
    This principle states that a class should never be forced to implement an interface that it doesn’t use. In other words, clients shouldn’t be forced to depend on methods they do not use. Let us checkout an example:

    interface Employee{
        void thumbLogin();
        void thumbLogout();
        void reportWork();
    }
    
    class Developer implements Employee{
        public void thumbLogin(){
        }
    
        public void thumbLogout(){
        }
    
        public void reportWork(){
        }
    }
    
    class Designer implements Employee{
        public void thumbLogin(){
        }
    
        public void thumbLogout(){
        }
    
        public void reportWork(){
        }
    }
    
    class Peon implements Employee{
        public void thumbLogin(){
        }
    
        public void thumbLogout(){
        }
    
        public void reportWork(){
        }
    }

    In the above example, Peon do not have to report anyone about the whole days work. Hence, we need to segregate the reporting logic from other employee tasks. Here is how we do it:

    interface Employee{
        void thumbLogin();
        void thumbLogout();
    }
    interface Reporting{
        void reportWork();
    }
    class Developer implements Employee, Reporting{
        public void thumbLogin(){
        }
        public void thumbLogout(){
        }
        public void reportWork(){
        }
    }
    class Designer implements Employee, Reporting{
        public void thumbLogin(){
        }
        public void thumbLogout(){
        }
        public void reportWork(){
        }
    }
    class Peon implements Employee{
        public void thumbLogin(){
        }
        public void thumbLogout(){
        }
    }
  5. Dependency Inversion Principle( DIP )
    This principle states that higher level modules should not use low level modules directly. Instead there should be a layer of abstraction between the two. Also, this layer should not depend on details.

    A <=> B <=> C

    Here A module is using C module through B module. The application should be able to replace C with other modules without disturbing A and B. Similarly, the module C can also be used with other higher level modules.
    We wish to log messages in our application. The logging can be done to screen, file or database.
    Here is an example:

    class ScreenLogger{
        public void log(String message){
        }
    }
    
    class FileLogger{
        public void log(String message){
        }
    }
    
    class ExceptionLogger{
        public void logToScreen( String message ){
            ScreenLogger logger = new ScreenLogger();
            logger.log(message);
        }
    
        public void logToFile( File file ){
            FileLogger logger = new FileLogger();
            logger.log(message);
        }
    }
    
    class Test{
        public void task(){
            try{
            }
            catch(FileNotFoundException ex){
                 new ExceptionLogger().logToScreen(ex.getMessage());
            }
            catch(IOException ex){
                 new ExceptionLogger().logToFile(ex.getMessage());
            }
        }
    }

    Now if we need to log to database, we need to add one more method to the Logger class. This leads to modification to the Logger class again and again. So, we need to design the application as follows:

    Application <=> Logging Abstraction <=> Logging Logic

    Here is the updated code:

    interface Logger{
        void log(String message);
    }
    
    class ScreenLogger implements Logger{
        public void log(String message){
        }
    }
    
    class FileLogger implements Logger{
        public void log(String message){
        }
    }
    
    class ExceptionLogger{
        private Logger logger;
    
        public ExceptionLogger(Logger logger){
            this.logger = logger;
        }
        public void log( String message ){
            logger.log(message);
        }
    }
    
    class Test{
        public void task(){
            try{
            }
            catch(FileNotFoundException ex){
                 ExceptionLogger logger = new ExceptionLogger(new ScrenLogger());
                 logger.log(ex.getMessage());
            }
            catch(IOException ex){
                 ExceptionLogger logger = new ExceptionLogger(new FileLogger());
                 logger.log(ex.getMessage());
            }
        }
    }