SOLID Principles

SOLID Principles

A brief introduction to Software Application Design using Object-Oriented Programming and SOLID Principle approach.

The SOLID Principles was proposed by Robert C. Martins (a.k.a Uncle Bob). The SOLID principles have changed the Object-Oriented Programming world forever. The word SOLID is an acronym that has five (5) different meanings, namely:

  • Single Responsibility principle.
  • Open/Closed principle.
  • Liskov's principle.
  • Interface Segregation principle.
  • Dependency inversion principle.

    These principles ensure that software designs are easy to understand, debug, and modularize. They also help to improve code maintainability and readability. Now, let us look at them individually with some code snippets in Java

    SINGLE RESPONSIBILITY:

    This principle states that a class must do one thing and one thing only. This means that a class must not be found doing too many unrelated things at the same time. This single responsibility makes it easy to test a class and its methods. This does not imply that a Class should have only one method, it only emphasizes that the methods of a class should be similar to the single purpose of that class.

    For instance,

Class Animal{

public int addAnimal(){...}
public String spreadingDiseases() {...}
public int calculateTotalDogs(){...}

}

The above code snippet shows that the Animal Class has three functionalities: addAnimal(), spreadingDiseases(), calculateTotalDogs() that are unrelated which violates the SOLID PRINCIPLES. To correct this, we split this class into three different classes to achieve the SOLID PRINCIPLES as follows:

Class Animal {
public int addAnimal(){...}
}
Class SpreadingAnimalDiseases{
public String spreadingDiseases() {...}
}
Class TotalDogsCalculation{
public int calculateTotalDogs(){...}
}

OPEN/ CLOSED PRINCIPLE:

This principle states that the application should be open to extension but closed for modifications. The open to extension means that our class is designed in such a way that it can accommodate more functionalities as new requirements are generated on the go! Based on this extension, we can further implement new functionalities to the module. But close for modification means that once a class has been developed, it cannot be modified except for debugging purposes. This sounds contradictory but the message here is, once you structure your classes and their dependencies correctly, you can always add extra functionalities without disrupting the source code that has been written, tested, and debugged. As such, you also reduce the chances of adding new bugs to the code which results in more robust software. These functionalities are added by creating new classes that implement this interface. Let's consider the following examples done using the code snippets below:

Supposed that StudentInfo is a class that has method studentNumber() that returns student number

public StudentInfo{
pulic int studentNumber(Student std){

if (std instanceOf PartTimeStudent){
return std.getNumber();
}
if (std instanceOf FullTimeStudent){
return std.getNumber();
}

}
}

Notice that, if we intend to add another subclass like GraduateStudent, we would attempt to do this by adding an extra if statement which violates the Open/Closed Principle (OCP).

A good attempt to achieve this is by overriding the studentNumber method as shown below:

public StudentInfo{
pulic int studentNumber()
{
     //functionality
}
pulic class PartTimestudent extends StudentInfo(){
pulic int studentNumber(){

return this.getValue();

}

}
pulic class PartTimestudent extends GraduateStudent{
pulic int studentNumber(){

return this.getValue();
}

}
}

This approach does not affect the application.

LISKOV'S SUBSTITUTION PRINCIPLE:

This principle was proposed by Barbara Liskov and it applies to Inheritance hierarchy. Here, subclasses or derived classes are expected to have a corresponding behavior with their Super or Base classes. When a subclass does something that is unexpected by the supertype, this results in a violation of LSB's Principle. For instance, imagine a subclass throwing an exception that the superclass does not throw. This means that derived or subclasses should not do less or more than their superclasses. Another instance is, imagine a superclass as a Rectangle, whose area is:

(Area of Rectangle = Length x Breadth),

with a subclass as Square, whose area is:

(Area of Square= Length X Length).

This results in a violation of LSB's Principle as the Rectangle superclass requires two unequal sides to implement its Area, while the Square subclass requires less. This is a case of a subclass doing less than its superclass. LSB Principle emphasizes the need for subclasses to be completely substitutable with their superclasses. So, if Class Y is a subclass of Class Z, we should be able to replace Z with Y without disrupting the behavior of the program. The emphasis here is on the behavior of the subclass and its superclass, and this principle can be viewed as an extension of the Open/Closed Principle (OCP). This LSB Principle guides us in designing our classes to preserve the existing behaviors except in rare cases where we have to do otherwise. Let's consider the following Java code snippet as an illustration:

public class Rectangle{
private double length;
private double breadth;

public double area();

public setLength(double length);
public setBreadth(double breadth);

}

This violates LSB's Principle which could lead to undefined behavior and take weeks of debugging.

public class Square extends Rectangle{
public setLength(){

super.length(length);
super.breadth(length);
}
public void setBreadth(double breadth){
     setLength(breadth);
}

}

INTERFACE SEGREGATION:
This principle is built on the fact that Interface implementation is a contract/agreement and as such, clients must not be forced into a contract that involves using items that they do not make use of, rather there should be some sort of separation or segregation of these interfaces. The client should be allowed to choose methods that they only make use of. This is similar to the Single Responsibility Principle(SRP). Note that fewer interfaces are easier to implement leading to increased robustness of the system because of improved flexibility, and guaranteed reusability. Let's consider the following code snippet to further illustrate what we want to avoid:

public interface DeliveryOrder {
  askShippingAddress();
  informInvalidAddress();
  askForShipNumber();
  informInvalidShippingNumber();
  informTransactionDeclined();
  askForDestination();
  informNotEnoughMoneyInAccount();
  informDateOfDelivery();
  informDelivered();
}

We can split our DeliveryOrder interface further to ensure that the Delivery functionality depends on separate DeliveryOrder.

public interface PlaceDeliveryOrder {
  askShippingAddress();
  informInvalidAddress();
  askForShipNumber();
  informInvalidShippingNumber();
  askForDestination();    
}

public interface PaymentForDeliveryOrder{
   informNotEnoughMoneyInAccount();
   askForFeeConfirmation();
   informDelivered();
}

public class DeliveryAuthentication implements PlaceDeliveryOrder, PaymentForDeliveryOrder {
      ...
}

DEPENDENCY INVERSION PRINCIPLE:
The Dependency Injection Principle(DIP) states that we must use abstraction (abstract classes and interfaces) than concrete implementations for some obvious reasons like decoupling and flexibility. This makes our design easier to change. As a Principle, high-level modules should not depend on low-level modules but rather depend on abstraction. DIP enables us to test things in isolation. Let's consider the following code snippet below to further buttress our point:

public class PlayNintendoGame  
{  
//functionality 
}

From the above class, it is clear that we may require some game consoles like joystick, cartridges to play our Nintendo game. To solve this problem, we create a constructor of our PlayNintendoGame class and add the joystick and cartridge instance.

public class PlayNintendoGame  
{  
public final joystick;  
public final cartridge; 

public PlayNintendoGame()  
{  
joyStick = new  joystick();     //instance of joystick class  
cartridge = new cartridge(); //instance of cartridge class  
}  
}

We can now comfortably play our NintendoGame with the help of a joystick and cartridge. However, we still face one more challenge here as all our three classes has become more tightly coupled together using the new keyword making it difficult to test the PlayNintendoGame class. So we need to decouple our PlayNintendoGame class from the joystick interface. We achieve this by using the joystick interface and this keyword.

public interface joyStick   
{   
//functionality  
}
public class PlayNintendoGame
{  
private final Joystick joystick;  
private final Cartridge cartridge;  
public PlayNintendoGame(JoyStick joyStick,  Cartridge cartridge)   
{  
this.joystick = joystick;  
this.cartridge = cartridge;  
}  
}

From the above code, we have successfully used the dependency injection principle to add the joystick dependency in the PlayNintendoGame class. We have successfully decoupled this class! Yipee!

CONCLUSION:
SOLID Principles are vital tools that we should always bear in mind when designing a feature or an application. We have seen how they help to reduce dependencies making it easy for us to make changes in our code without affecting the entire system-this makes software design easier, clearer, reusable, scalable, and more maintainable. SOLID Principles aligns with one of the most efficient methods of Software Development Lifecycle, which is the Agile Methodology. Agile SDLC approach standouts because it encourages incremental changes and is more cost-effective than the Waterfall Model. Next time you want to design softwares, let these five principles guide your design. By applying these principles, the code you write will be much more testable, scalable, and extendable.

Linkedin photo resized.jpeg

PETER AIDELOJE is a passionate Solution provider whose interest aligns towards the Software Architecture, DevOps, and Cloud Computing/Security field. He is currently having fun with Java Programming language (SpringBoot Framework), CSharp, and ASP.Net framework. Whenever Peter isn't coding, you find him writing/drafting an article about a topic in the Tech space. Peter enjoys engaging and sharing his knowledge with other developers via his social handles.

Kindly reach out to me via any of my socials via the links below:

LinkedIn Profile
GitHub Profile