S.O.L.I.D Principles in OO programming

S.O.L.I.D Principles in OO programming

SOLID is the first five essential principles of object oriented programming for building a well designed, working software. To build a well designed and working software, we as developers and software engineers must have a low coupling, high cohesion, strong encapsulation. SOLID principles help the developers achieve those qualities.

Software engineers applying those principles can achieve to develop a software that is robust, and easy to maintain, reuse and extend over time, leading to good scalability. Let me clarify first the SOLID acronym, it stands for

S - Single Responsibility Principle

O - Open Close Principle

L - Liskov Substitution Principle

I - Interface Segregation Principle

D - Dependency Inversion Principle

Lets look thru each individual principles, and how they each help the developers and software engineers to build the robust software.

Single Responsibility Principle

There should never be more than one reason for a class to change.

What it means by that phrase, the class should have only one responsibility to change. To be more precise, every module or class should have responsibility over a single part of the functionality provided by software, and that responsibility should be encapsulated by the class. And there are several motives for applying the SRP.

  • Maintainability - As a software grows over its life time, it becomes more and more challenging to maintain. Even worse case (from my personal experience), when there is not an existing documentation or documentation is out of synch or the requirements frequently change, the existing code should undergo some reconstruction. It leads to modification of classes. It automatically implies that the more responsibility the class has, the more change requests it gets, and the much harder those changes will be to implement. Most of the times, the responsibilities of the class are tight coupled to each other, changes in one of the responsibilities of the class will result in additional potential changes in other responsibilities to be handles properly by the class.
  • Testability - Test driven development is very important in software development. By having that quality, it is easier to implement the test driven programming.
  • Flexibility and Extensibility - As you have noticed, SRP enables flexibility and extensibility.

Besides, SRP provides the features like parallel development and loose coupling. One may wonder how to implement such a class. You may define the class that has the following qualities: 1. Every class should have focus on single task at a time. 2 . Everything in the class should relate to that single purpose. 3. There can be as many as members and functions as long as they are related to that one single responsibility. With these, we can achieve smaller and clean classes. The code becomes less fragile.

Let me make those mentioned above simpler, let's assume that we are building the software that enables the users to login and sign up. It will be the better practice to separate out the responsibilities of signing and login in two separate classes, and from the other parts of the software. And we should always be ready for external changes.

By doing so, we can achieve looser coupling between dependencies classes, and better readability, and lower complexity. As you already know, the coupled objects leads to more fragile code base which is hard to refactor when requirements change.

Let us take an example of User interface which has functionalities for login(), register(), logError(), sendEmail(). At first glance, it seems it is totally fine for the users to have these responsibilities

public interface IUser {

    boolean login(String username, String password);

    boolean register(String username, String password, String email);

    void logError(String error);

    void sendEmail(String emailContext);
}
 
  

However, we look at it very carefully, we realize that users should not worry about logError and sendEmails. The users main concern must be only login and register. We do not want those two features logError and sendEmails to become a part of IUser. So, we can break it down into three separate interfaces. As in the below examples. It makes more sense this way

public interface IUser {

    boolean login(String username, String password);

    boolean register(String username, String password, String email);
    
}
 
  
public interface ILogger {

    void logError(String error);
}
 
  
public interface IEmail {
    void sendEmail(String emailContext);
}
 
  

All classes will have their own responsibilities

Open - Closed Principle

Software entities (classes, modules, functions, etc) should be open for an extension, but closed for modification.

What it means by this principle is that the classes/entities, modules, and packages can be extended and new functionalities can be added to them by extension but not modification of the old source code. This ensures that the class behavior can be extended, and as the requirements change in the future, we should be able to make a class in new and different ways, to meet the needs of the requirements. However, the source code of such classes are set in stone, no one can make changes to the source code.

There are several ways to achieve this OCP. One of them is by abstraction. What it means is that the clients should be able to implement the new functionalities on derived classes or access the original classes by interface.

Let us see the simple an example here. Imagine we have employee class with one functionality to compute the salary of the current employee.

public class Employee {

    private String employeeId;
    private String employeeName;
    private double bonus = 500;
    public double computeSalary(double salary) {
        return salary + bonus;
    }
}
 
  

As you can see the code works perfect, and can't see any issue with it. And imagine the scenario wherein the requirement changes, and we have another type of employee "Contract Employee". And we need to modify the existing code by adding employeeType and modify the compute salary functionality accordingly as given below:

public class Employee {

    private String employeeId;
    private String employeeName;
    private double bonus = 500;

    private String employeeType;

    public double computeSalary(double salary) {
        if (employeeType.equals("contract")) {
            return salary + 100;
        } else {
            return salary + bonus;
        }
    }
}
 
  

When we test the code, it works fine. However, as the requirements keep changing, we end up changing the the same class over and over again, eventually we will find ourselves breaking SRP as we have talked in the first part, causing problems for maintenance.

How do we need to address this issue. The solution is very simple by decoupling the classes by abstraction. Need to create the employee interface or abstract classes, and leave the compute salary functionality as an abstract for the concrete implementation for derived classes.

public abstract class EmployeeAbstract {
    private String employeeId;
    private String employeeName;
    
    public abstract double computeSalary(double salary);
}
 
  
public class PermanantEmployee extends EmployeeAbstract {
    @Override
    public double computeSalary(double salary) {
        return salary + 100;
    }
}
 
  
public class ContractEmployee extends EmployeeAbstract {
    @Override
    public double computeSalary(double salary) {
        return salary + 100;
    }
}
 
  

Now we can say that EmployeeAbstract class is open for an extension but closed for modification following the OCP. As you may realize, by application of the open-closed principles you will get a loose coupling, you will improve the readability and most importantly you will reduce the risk of breaking existing functionality in the old source code.

Liskov Substitution Principle

Derived classes must be substitutable for their base classes.

The idea is here the objects should be replaceable by the instances of their subtypes, and without affecting the functioning of the systems from the client side. It means, instead of using the actual implementation, we should be able to use the base class and get the expected result. Most software engineer's mistake while creating classes is to focus on the properties of the class, rather than focusing more on behaviors of the classes/objects.

This principles confirms that our abstraction is correct and helps us get a code that is easy reusable, and class hierarchies that are easily understood. This principle LSP closely is related to OCP. The violation of LSP is actually violation of OCP in theory.

Let us see an example to clarify more on it. For sake of simplicity, we consider the same example we have seen above. We have Employee class, from which ContractEmployee and PermanentEmployee are derived. It follows the LSP since ContractEmployee and PermanentEmployee are actually Employee. So we can substitute them as in the following example:

public class Client {
    public static void main(String[] args) {
        EmployeeAbstract employee = new ContractEmployee();
        EmployeeAbstract employee2 = new PermanantEmployee();
    }
}
 
  

Interface Segregation Principle

The classes that implement the interfaces, shouldn't be forced to implement the methods they don't use.

In the other words, we can say it is better to have many specific interfaces than fewer/fatter interfaces. ISP is all about how to define and build the interfaces. Once we realize the interfaces are becoming too large and fat, we need to split them into interfaces which are more specific and smaller. The interfaces are implemented by the clients, and the clients should know only about the methods that are related to them. If you add the irrelevant methods into the interfaces, the clients have to implement them as well. That's why the clients should not be dependent on the interfaces they do not use.

IPS's main intent is to keep the systems decoupled, and thus easier to refactor, change and deploy. Let us make it clearer by an historical example. Xerox company created a new printer system that can do several different tasks such as stapling and faxing along with regular tasks. The software for this printer was developed from the scratch, and as we add more functionalities, the maintenance and deployment of the modification to the system becomes more complex. The solution was found which is one large job class can be segregated into multiple classes depending on the requirement. If you have realized this is very close to SRP.

Let's assume the printer does the following tasks

public interface IPrintTask {
    boolean printContent(String content);

    boolean scanContext(String context);

    boolean faxContent(String context);

    boolean photoCopy(String context);
}
 
  

And now my imaginary LaserMacPrinter will implement this interface

public class LaserMacPrinter  implements IPrintTask{
    @Override
    public boolean printContent(String content) {
        return true;
    }

    @Override
    public boolean scanContext(String context) {
        return true;
    }

    @Override
    public boolean faxContent(String context) {
        return true;
    }

    @Override
    public boolean photoCopy(String context) {
        return true;
    }
}
 
  

Let's imagine we have another Printer that can implement only certain methods, then we automatically stuck implementing all the methods. It is one of the common problems in daily programming. Simple solution is to segregate every method into simpler smaller interfaces. Hurray!

Dependency Inversion Principle

High level modules should not depend on low level modules rather both should depend on abstraction. Abstraction should not depend on details; rather detail should depend on abstraction.

In the other words, we can say the abstraction should not depend on details, the details should depend on the abstraction. DIP is mainly about how to reduce the dependencies among the code modules. If the implementation detail will depend on the higher level abstraction, it will get you a system that is coupled correctly. It will be also great impact on encapsulation and cohesion of the system.

Let us see simple but powerful examples for this. Let us image a system that allows the authentication thru external service such as google, instagram or github. We have relevant classes for each and we would like to implement them in some places in the system to authenticate the users. To implement those, we have two choices. We can write a code that adapts each service or we can abstract the authentication process. The first solution is a dirty solution, causing the issue in the future when a new authentication process is added. The second is much better and cleaner, it allows for the future addition of services, the change can be done to each service without changing its integration logic.

In Summary

When developing any software, there are two concepts that are very important: cohesion (when different parts of a system will work together to get better results than if each part would be working individually) & coupling (can be seen as a degree of dependence of a class, method or any other software entity). Following the SOLID Principles gives us many benefits, they make our system reusable, maintainable, scalable, testable and more.

Hurray, you did a great job reading this!

Thank you for reading!


要查看或添加评论,请登录

社区洞察

其他会员也浏览了