SOLID Programming Principles in OOPS
In any object oriented programming language, classes are the main building blocks of the application. In order to make the application robust it should be easily modifiable or extendable with future enhancements and these building blocks (classes) should be well written and easily maintainable. In case these building blocks are not well written lot of problems with respect to design aspects will surface later in the design phase where it will be difficult to handle the issues.
In order to overcome these problems associated with the classes in any object oriented programming languages, there are 5 standard set of principles that can be followed. Implementing these set of standard principles will definitely enhance the coding process and will lead to lesser bugs in the production. These 5 standard principles are termed as SOLID. Where each letter stands for:
- S -> Single Responsibility Principle
- O -> Open Closed Principle
- L -> Liskov’s Substitution Principle
- I -> Interface Segregation Principle
- D -> Dependency Inversion Principle
The above SOLID principles are also part of the standard and best programming practices in any object oriented programming languages.
This SOLID programming principles were introduced by Michael Feathers which was initially derived by Robert Martin in early 2000 which was known as “First Five Principles”. By applying the above principles by any programmer will definitely make sure that the system developed will be easy to maintain and can be easily extended in future for any design changes.
Let us see each principle in detail with a small example:
Single Responsibility Principle (SRP)
- “A class should have only single responsibility” i.e. Programmer should write, modify and maintain a class only for one purpose. This will ensure that there will be flexibility to modify the class in future without thinking about the impacts of the changes on other entities. For e.g. if a model is written then it should represent only one actor/entity
- Responsibility is a reason to change and there should be only ONE reason to change.
- This principle helps in a good way of identifying the classes during the design phase of an application.
Example:
Consider the below sample Student class, which has the provision to display the student name, register no. and course he/she is pursuing in a college. But, observe that there is one more method save() which saves the student details into the database.
In the above example whenever there is a change needs to be made on the save() method, then this class needs to be modified. Hence, moving this save method to a new class will ensure that the responsibilities are separated and it will be easy to modify the save method without affecting the Student class.
SRP leads to a loosely coupled design with less and lighter dependencies.
Open Closed Principle (OCP)
- “Software entities (Classes, modules & functions/methods), should be open for extension and closed for modification”. This principle says that any developer should design in such a way that the classes when any other developers wants to change the flow/design control for a given set of conditions, they should always extend that class and override the required functions. At any point of time, no developer should be allowed to modify the same class for some extension.
- The best examples that can be considered here is the frameworks like spring and struts. Developers who work on these frameworks will never be allowed to change the core logic implemented. They will have to extend the classes and override the same to implement the required behaviors.
- It should be noted clearly that modifying an existing class for a new change will introduce a new level of abstraction which leads to the increase in the code complexity.
Example:
Consider a simple insurance claim and approval process that happens in general. In the below example the class HealthInsurance.java is a type of insurance which needs to be processed for the approval and claim. Another class InsuranceClaimApprover.java, which process, validates and approves claim for the reimbursement.
In this InsuranceClaimApprover.java, the design will work fine till the method processHealthClaim() receives the object of the type HealthInsurance which is of the concrete one. But, when a new type of claim for e.g. Vehicle insurance needs to be added, creating a VehicleInsurance.java does not create any problem. But, the class InsuranceClaimApprover needs to be modified to consider the VehiclInsurance as well.
This is how the modified InsuranceClaimApprover looks like:
Apparently this is clearly violating the open closed principle. Because, whenever a new type of insurance needs to be included as part of the customer demands, we need to keep modifying the InsuranceClaimAprrover class which is not to be done.
Hence, to achieve the OCP, a new layer of abstraction needs to be introduced as shown below:
Introduce an abstract class called Insurance:
Modified HealthInsurance and VehicleInsurance classes:
Modified InsuranceClaimApprover.java
This OCP should be applied to those areas of the modules which are most likely to be changed.
Liskov’s Substitution Principle (LSP)
- “Derived types must be completely substitutable for their base types”. This principle states that the new classes derived by a developer should just extend the base class without breaking the existing functionality of the base class. Otherwise the newly derived classes may cause some serious problems when they are used with the existing modules.
- This principle in general is an extension of the Open/Closed principle.
- This principle will ensure that the derived class will are extending the base classes without modifying the behavior.
Example:
Consider an interface IProcessData which contains 2 methods loadData() and persistData(). Assume 2 classes TravelerUserProcessData and AdminUserProcessData implements this interface and of course things will work perfectly fine.
Everything will work perfectly fine till a new class is introduced where the implementation for persistData() methods throws a CustomException. Assume there is a class which has a loadAll() method, used to load all the classes along with the other classes. When the application is run, all the classes will be loaded properly except one class SuperAdminUserProcessData which throws the CustomException.
Looking at this, the app may blow up when tried to save all the classes with their details, which in turns concludes that with this design “the object should be substitutable by its base class” but, which is not happening in this case.
Hence, the fix here is to modify the interface based on what each client requirement. i.e. the best approach is to put the methods loadData() and persistData() into 2 different interfaces.
Interface Segregation Principle
- “Clients should not be forced to depend upon interfaces that they do not use”. This principle basically applicable to the interfaces as single responsibility principle.
- This basically states to have many small interfaces based on group of methods, which will serve one sub module.
- This enforces to have a decoupled system which will be easier to refactor and maintain.
Example:
Consider an interface called IDigitalPrinter which has the methods like print, scan and xerox. Whichever the class implements this interface should be provide the implementations to the methods of this interface. Consider a client, who asks for the functionality of print. So, what do be done with the other 2 methods. It is as good as useful for nothing.
So, clients should not be forced rely on the interfaces which they do not require at all. Hence, it is always a better and good practice to breakdown the interfaces into multiple special purpose interfaces as shown below.
Since, the interface is now broken down into multiple special interfaces, client will be provided an option to choose which features to be implemented.
Hence, interface segregation helps implementation to be kept simple and makes the code very much easy to debug and maintain.
Dependency Inversion Principle
- “Depend on abstractions, not on concretions”. This principle states that the application should be designed in such a way that various modules can be separated from each other by using an abstract layer.
- Consider the BeanFactory in the spring framework. This layer is completely abstracted. Developer job is to provide the object details which needs an instance to this factory. Rest of the job will be handled by this BeanFactory. Here developer is dependent on the abstraction not on the implementation (concretion).
Example:
Consider a class called Employee with a method work() as shown below:
Another class Manager manages the employee object
Now consider that the manager class will be having a quite complex logic. Assume a new worker “Super Worker” needs to be introduced by the Manager class.
Now, in order to include the super worker class in the Manager class, this SuperWorker needs to be defined in the Manager class which will make the Manager class to become more complex as the logic grows. And also, whenever a new type of worker in introduced, the Manager class also needs to be changed which is not advisable. Also, here the high level classes depends on the low level class which is again results in bad design as the classes should depend on abstraction and not concretion !!
In order to make the design efficient, have an interface which will be implemented by the different types of worker classes. With this whenever a new type of worker is introduced, the high level class i.e. Manager class will not be modified!!
Modified Manager class
With this approach, the Manager class will be unchanged whenever a new type of worker is introduced as the dependency is now on abstraction not with the concretion!
Questions? Comments? Suggestions? Let us know!! Like / Subscribe / Follow for more updates.