Software Development Principles (SOLID)

SOLID Software Design Principles

SOLID Software Design Principles

In the ever-evolving world of software development, writing code that is easy to maintain, extend, and scale has become a top priority. The SOLID principles offer a set of time-tested guidelines that help developers design robust, flexible, and maintainable software systems. First introduced by Robert C. Martin, also known as Uncle Bob, these principles act as a foundation for crafting high-quality object-oriented code. In this blog, we will explore the five SOLID principles and understand how they contribute to writing better software.

ProtoTech Solutions expertise is a coding standard that all developers should have a clear concept to develop a readable and maintainable program. It was promoted by Robert C Martin (Uncle Bob) and is used across the object-oriented design spectrum.

Advantages of using SOLID Principles: It makes the code more extendable, logical and easier to read.

Disadvantages if we don’t follow the Solid Principles: When the developer builds a software without following the solid principles he can face below problems:

  • The code can become inflexible and more brittle.
  • Small changes in the software can result in bugs.

Goal Of The Principles

The goal of the Solid principles is the creation of the mid-level software structures that:

  • Tolerate change
  • Are easy to understand
  • Are the basis of components that can be used in many software systems.

SOLID is the most widely used sets of object-oriented design principles and It’s a mnemonic acronym for the 5 design principles of development  given below:

  1. S: Single Responsibility Principle(SRP)
  2. O: Open/Closed Principle(OCP)
  3. L: Liskov Substitution Principle(LSP)
  4. I: Interface Segregation Principle(ISP)
  5. D: Dependency Inversion Principle(DIP)

Software Development Principles SOLID

The SOLID principles are related to classes. It tells us how to arrange our functions and data structures into classes, and how those classes should be interconnected. If we followed these principles, then it will surely improve the quality of code and will also help us to understand the most well-designed software. We will see all the principles one by one.

The Single Responsibility Principle (SRP)

According to this principle “A class must handle one and one responsibility”. In this context, responsibility means one reason to change. If a class has more than one reason to change then we must split the functionality from that class into two different classes where one class should handle only one responsibility. And in the future, if need to make any changes in one responsibility then we will make it in the class which handles it, without affecting the other responsibility. In the case of class with multiple responsibilities, changes made in the class for one responsibility will surely affect the functionality of other responsibility. Consider the Object3D class example from a CAD Application:

class Object3D
{
changeColor();
getVertices();
changeName();
}

This class violates SRP because those three methods are responsible for three very different changes.

  1. The changeColor() method is related to Texture.
  2. The getVertices() method is related to actual geometry.
  3. The changeName() method is specific to geometry attributes.

 

By putting the source code for these three methods into a single Object3Dclass, the developer has coupled each of these three responsibilities with each other unnecessarily.

We can solve this problem by separating methods into different classes:

Class Texture
{
changeColor();
}
Class Geometry3D
{
getVertices();
}
Class Attributes
{
changeName();
}

The three classes are not allowed to know each other and each class holds only the source code necessary for its particular function.

The Open-Closed Principle (OCP)

This principle states that “A software artifact should be open for extension but closed for modification”. The OCP is one of the driving forces behind the architecture of systems. The goal is to make the system easy to extend without incurring the high impact of change. This goal is accomplished by partitioning the system into components and arranging those components into dependency hierarchy that protects higher level components from changes in lower-level components. Software entities (classes, modules, functions, etc.) be extendable without actually changing the contents of the class you’re extending. If we follow this principle strongly, then it is possible to modify the behavior of our code without ever touching a piece of the original code. Consider the below example of adding different types of light effects in a CAD application:

class AddLightEffects{
           void InsertLight(Light l) {
                       if (l.m_type==1)
                                   InsertAmbientLight(l);
                       else if (l.m_type==2)
                                   InsertSpotLight(l);
           }
           void InsertAmbientLight(AmbientLight a) {….}
           void InsertSpotLight(SpotLight s) {….}
}
class Light{
           int m_type;
}
class AmbientLight: public Light{
          AmbientLight() {
                       m_type=1;
           }
}
class SpotLight: public Light{
           SpotLight() {
                      m_type=2;
           }
}

Here, we have written the light insertion code in AddLightEffects class which is higher level class. If we want to support any new light in this structure then we need to modify a high-level AddLightEffects class. This violates the OCP principle. We can solve this problem by overriding the InsertShape method in derived classes as shown below.

class AddLightEffects
{
           void InsertLightEffects(Light l)
{
                      l.InsertLight();
}
}
class Light
{
           int m_type;
virtual void InsertLight(Light l) ;
}
class AmbientLight: public Light{
           AmbientLight() {
                       m_type=1;
           }
void InsertLight(Light l)
{
//Ambient light insertion code
}
}
class SpotLight: public Light{
           SpotLight() {
                       m_type=2;
           }
void InsertShape(Light l)
{
//spotlight insertion code
}
}

Following the OCP principle, we can extend the Light class to support as many lights as possible without modifying the high-level classes.

The Liskov Substitution Principle (LSP)

In programming, the Liskov substitution principle states that if S is a subtype of T, then objects of type T may be replaced (or substituted) with objects of type S. More generally it states that “objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program”.

Below is an example for which the LSP is violated. Here two classes Rectangle and Square are used. Square class is inherited from the Rectangle class. Based on some conditions, the square class is returned by a factory pattern. And we are unaware about, exactly what type of object will be returned. But we know it’s a Rectangle. For the rectangle object, set the width to 5 and height to 10 and get the area. Here, the area should be 50. Instead, the result will be 100.

// Violation of Liskov’s Substitution Principle

class Rectangle
{
protected :
           int m_width;
           int m_height;
public
           void setWidth(int width){
                       m_width = width;
           }
           void setHeight(int height){
                       m_height = height;
           }
           int getArea(){
                       return m_width * m_height;
           }          
}
class Square : public Rectangle
{
Public:
void setWidth(int width){
                       m_width = width;
                       m_height = width;
           }
           void setHeight(int height){
                      m_width = height;
                       m_height = height;
           }
}
class Shape
{
public:
           static Rectangle GetNewRectangle()
           {
                       // it can be an object returned by some factory …
                      return new Square();
           }
           static void main (String args[])
           {
                       Rectangle r = this.GetNewRectangle();
                    r.setWidth(5);
                       r.setHeight(10);
                       // Here, r is a rectangle.
                      // user able to set the width and height for the base class
                    System.out.println(r.getArea());
                       // user surprised to see that the area is 100 instead of 50.
           }
}

The Interface Segregation Principle (ISP)

This principle states that “Clients should not be forced to depend upon interfaces that they don’t use”. When we design an application we must take care of how we are going to abstract the programming module which has several sub-modules. If we made the abstract class by considering only one module, and in another sub-module, we need only a few methods from the abstract class then, in that case, we need to write some dummy code for the interface that is not useful.  Such an interface with dummy code is called a fat/polluted interface. And having a polluted interface in the programming module is not good, it can affect the functionality in an undefined manner. This principle advice software designers to avoid depending on things that they don’t use.

Consider the example below,  which have SectionPlane class with three interface methods   Insert_XPlane(), Insert_YPlane and Insert_ZPlane(). We have two derived classes XZSection and YZSection. For XZSection class we need only Insert_XPlane and Insert_ZPlane. But, because of the current programming module structure, we are forced to write some dummy code for Insert_YPlane interface.  Similarly, in the case of YZSection, we need to write some dummy code for Insert_XPlane interface.

// interface segregation principle – bad example

Class SectionPlanes{
virtual void Insert_XPlane();
           Virtual void Insert_YPlane();
Virtual void Insert_ZPlane();
}
class XZSection: public SectionPlanes{
           void Insert_XPlane() {
                       // ….x plane insertion code   }
           void Insert_YPlane() {
                      // …… dummy code
           }
void Insert_ZPlane() {
                       // …… z plane insertion code
           }
}
class YZSection: public SectionPlanes{
           void Insert_XPlane() {
                       //  …… dummy code }
           void Insert_YPlane() {
                       // …… y plane insertion code
           }
void Insert_ZPlane() {
                       // …… z plane insertion code
           }
}

 By following the Interface Segregation Principle. We can split the SectionPlanes interface into 3 different classes and we will override only required interface.

/ interface segregation principle – good example

Class XSection{
Virtual void Insert_XPlane();
}
Class YSection{
  Virtual void Insert_YPlane();
}
Class ZSection{
 Virtual void Insert_ZPlane();
}
class XZSection: public XSection,ZSection{
void Insert_XPlane() {
// ….x plane insertion code
  }
 void Insert_ZPlane() {
 //….z plane insertion code
 }
}
class YZSection: public YSection,ZSection{
 void Insert_YPlane() {
// ….y plane insertion code
}
void Insert_ZPlane() {
//….z plane insertion code
 }
}

The Dependency Inversion Principle (DIP)

This Principle States that “High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions”.   When we develop a programming module, first we write the low-level modules then depending on low-level modules we used to write high-level modules. This is a general and logical way but it is not flexible. Because in future if we need to change the low-level module then accordingly we need to modify high-level module also. Which is not acceptable. So DIP advice to write high-level module first then depending on high-level module write the abstract class and depending on abstract class write the low-level modules.

Below is a bad example which violates the DIP. Here, we have a high-level DimensionManager class and the low-level LinearDimension class. And we need to add a new module in our structure for AngularDimension.

// Dependency Inversion Principle – Bad example

class LinearDimension{
void InsertDimension() {
// ….linear dimension insertion code         
}
}
class DimensionManager
{
LinearDimension dim;
void setDimension (Dimension  d) {
                        Dim = d;
            }
            void manage() {
             dim.InsertDimension();
         }
}
class AngularDimension{
          void InsertDimension() {
//…. Angular dimension insertion code
}
}

Consider the dimension manager class is quite complex with very complex logic. And now we have to change it in order to introduce the Angular type Dimension. Some of the current functionality from the dimension manager class will get affected by this modification. Also, need to redo the unit testing

Solving the above problems will take time and the modification might induce new errors in the old functionality. With the following DIP principle, we will never face this situation and problems.

Below code supports the Dependency Inversion Principle. In this new design, a new abstraction layer is added through the Dimension Interface. So, the problems from the above code got resolved without changing the high-level logic.

// Dependency Inversion Principle – Good example

Class Dimension{
Virtual void InsertDimension();
}
class AngularDimension: public Dimension{
void InsertDimension() {
//  ….angular dimension insertion code
}
}
class LinearDimension : public Dimension{
void InsertDimension() {
//…..linear dimension insertion code
}
}
class DimensionManager {
Dimension dim;
void setDimension (Dimension d) {
dim= d;
}
void manage() {
dim.InsertDimension();
}
}

Conclusion

The SOLID principles provide a strong foundation for writing maintainable and extensible software. By applying these principles, developers can produce code that is easy to understand, modify, and test, leading to higher-quality software systems with reduced technical debt. Adhering to SOLID principles fosters a collaborative and agile development environment, making it easier for teams to work together and deliver innovative solutions to complex problems.

It will surely take some time to understand SOLID principles, but with steady practice and adherence, it will become a part of us and will greatly have a huge impact on the maintenance of our applications. So we can say These principles, when combined together, make it easy for a programmer to develop software that is easy to maintain and extend.

For more queries regarding any of the above-mentioned topics, feel free to connect with us on our website https://prototechsolutions.com, or email us at info@prototechsolutions.com