SOLID Principles

EL MAALMI
7 min readOct 6, 2023

--

Today, we will discuss the SOLID principles and demonstrate their implementation using Java.

Introduction:
It’s crucial for every Java developer or others out there to not only understand but also actively practice the SOLID principles in their day-to-day coding. These principles are fundamental for writing maintainable, scalable, and efficient code. Let’s explore each of the SOLID principles with examples in an easy-to-understand way.

1. Single Responsibility Principle (SRP):

SRP is a concepte that any single object on OOP should be made for one specific function.

Bad Version (Violating SRP):

On this example we Consider a class UserService which is responsible for both save user and process the payment This violates SRP.

public class UserService{
public void saveUser() {
// logic
}

public void processThePayment() {
// logic
}
}

Good Version (Adhering to SRP):

In the improved version, we separate the concerns of saving user data and process the payment into separate classes, each with a single responsibility.

public class UserService{
public void saveUser() {
// logic
}
}

public class PaymentService{
public void processThePayment() {
// logic
}
}

After refactoring our code to this versoin we found that is Improved for Maintainability and is Enhanced Reusability the Classes adhering to SRP are often more reusable. Since they have a well-defined purpose and are not tightly coupled to other concerns.

2. Open Closed Principle (OCP):

Software components such as classes and functions should be designed in a way that allows for the addition of new features without altering their existing codebase.

For better understanding, we’re going to explain it using the example below:

we have a Product class that calculates discounts based on the product type. However, this violates the OCP because adding a new product type requires modifying the existing Product class.

To add a new product type, like “premium,”we need to modify the Product class, which not respect the OCP.

public class Product {
private String type;
private double price;

public Product(String type, double price) {
this.type = type;
this.price = price;
}

public double calcDiscount() {
if (type.equals("standard")) {
return price * 0.1; // 10% discount
} else if (type.equals("vip")) {
return price * 0.2; // 20% discount
}
return 0.0; // No discount for other types
}
}

Good Version :

to impove this version of code to respect the OCP we use the separated CalcDiscount interface and specific discount calculator classes for each product type. This way, we can add new product types and discount calculators without modifying existing code.


public interface DiscountCalculator {
double calcDiscount(double price);
}

public class StandardDiscountCalculator implements DiscountCalculator {
@Override
public double calcDiscount(double price) {
return price * 0.1; // 10% discount
}
}

public class VIPDiscountCalculator implements DiscountCalculator {
@Override
public double calcDiscount(double price) {
return price * 0.2; // 20% discount
}
}

public class Product {
private String type;
private double price;
private DiscountCalculator discountCalculator;

public Product(String type, double price, DiscountCalculator discountCalculator) {
this.type = type;
this.price = price;
this.discountCalculator = discountCalculator;
}

public double calculateDiscount() {
return discountCalculator.calculateDiscount(price);
}
}

3. Liskov Substitution Principle (LSP):

The Liskov Substitution Principle (LSP) is a fundamental principle of object-oriented programming that promotes the correct use of inheritance and polymorphism.

For better understanding, we’re going to explain it using the example below:

In this example, we have a base class Bird with a fly method.

We then have a derived class Penguin that cannot fly. While the Penguin class overrides the fly method, it does not adhere to the LSP because it attempts to perform an action that is not applicable to penguins.

class Bird {
void fly() {
System.out.println("A bird is flying.");
}

}

class Penguin extends Bird {
@Override
void fly() {
System.out.println("A penguin is trying to fly."); // Penguins can't fly.
}
}

public class Main {
public static void main(String[] args) {
Bird bird = new Bird();
bird.fly(); // Output: "A bird is flying."

Penguin penguin = new Penguin();
penguin.fly(); // Output: "A penguin is trying to fly." (Incorrect behavior)
}
}

In the improved version, we adhere to the LSP by avoiding the override of the fly method in the Penguin class. Instead, we introduce a separate method, swim, to represent the penguin's behavior accurately.

class Bird {
void fly() {
System.out.println("A bird is flying.");
}
}

class Penguin extends Bird {
void swim() {
System.out.println("A penguin is swimming gracefully.");
}
}

public class Main {
public static void main(String[] args) {
Bird bird = new Bird();
bird.fly(); // Output: "A bird is flying."

Penguin penguin = new Penguin();
penguin.swim(); // Output: "A penguin is swimming gracefully."
}
}

4. Interface Segregation Principle (ISP):

  • This principle was first defined by Robert C. Martin as: “Clients should not be forced to depend upon interfaces that they do not use“.
  • The Interface Segregation Principle (ISP) in object-oriented design encourages the creation of smaller, more focused interfaces.

For better understanding, we’re going to explain it using the example below:

interface Vehicle {
void drive();
void fly();
}

class Car implements Vehicle {
public void drive() {
// Driving functionality
}

public void fly() {
// Unsupported flying functionality
}
}

class Plane implements Vehicle {
public void drive() {
// Unsupported driving functionality
}

public void fly() {
// Flying functionality
}
}

public class Main {
public static void main(String[] args) {
Vehicle car = new Car();
Vehicle plane = new Plane();

car.drive();
car.fly(); // Unsupported operation

plane.drive(); // Unsupported operation
plane.fly();
}
}

In this version, the Vehicle interface forces all implementing classes to provide both driving and flying methods, even if they do not support one of these actions.

Sow to improved this version of code, we adhere to the ISP by creating two separate interfaces, Drivable and Flyable, each focusing on a specific vehicle capability. Classes implement only the interfaces that correspond to their capabilities.

interface Drivable {
void drive();
}

interface Flyable {
void fly();
}

class Car implements Drivable {
public void drive() {
// Driving functionality
}
}

class Plane implements Flyable {
public void fly() {
// Flying functionality
}
}

public class Main {
public static void main(String[] args) {
Drivable car = new Car();
Flyable plane = new Plane();

car.drive();
// car.fly(); // Error: Unsupported operation
// plane.drive(); // Error: Unsupported operation
plane.fly();
}
}

5. Dependency Inversion Principle (DIP):

The Dependency Inversion Principle (DIP) advocates that higher-level modules should not rely on lower-level modules. Instead, both should rely on abstract, generalized structures.

In this example, we have a high-level module, LightSwitch, that directly depends on a low-level module, LightBulb. The LightSwitch class controls the state of the LightBulb, creating a tight coupling between them.

class LightBulb {
public void turnOn() {
System.out.println("Light bulb is on.");
}

public void turnOff() {
System.out.println("Light bulb is off.");
}
}

class LightSwitch {
private LightBulb bulb;
private Boolean on_f;

public LightSwitch() {
bulb = new LightBulb();
}

public void operate() {
if (!on_f) {
bulb.turnOn();
} else {
bulb.turnOff();
}
}
}

public class Main {
public static void main(String[] args) {
LightSwitch switch1 = new LightSwitch();
switch1.operate();
}
}

Sow to improved this version of code, we adhere to the DIP by introducing an interface, Switchable, that both LightSwitch and LightBulb implement. This allows LightSwitch to depend on an abstraction rather than a concrete implementation.

interface Switchable {
void turnOn();

void turnOff();
}

class LightBulb implements Switchable {
public void turnOn() {
System.out.println("Light bulb is on.");
}

public void turnOff() {
System.out.println("Light bulb is off.");
}
}

class LightSwitch {
private Switchable device;
private Boolean on_f;

public LightSwitch(Switchable device) {
this.device = device;
}

public void operate() {
if (!on_f) {
device.turnOn();
} else {
device.turnOff();
}
}
}

public class Main {
public static void main(String[] args) {
Switchable bulb = new LightBulb();
LightSwitch switch1 = new LightSwitch(bulb);
switch1.operate();
}
}

In this version, we adhere to the Dependency Inversion Principle by introducing the Switchable interface. The LightSwitch class depends on the Switchable abstraction, which allows us to inject different implementations (such as different types of bulbs) without modifying the LightSwitch class.

Conclusion:

The SOLID design principles constitute a set of guidelines aimed at fostering clear, manageable, and adaptable software architecture. By adhering to these principles, developers are empowered to craft code that is more comprehensible, extensible, and maintainable. This, in turn, contributes to the development of resilient and flexible software systems

--

--

EL MAALMI
EL MAALMI

Written by EL MAALMI

Passionate software engineering graduate skilled in Java Spring, JavaScript, clean code, React, and Docker and a junior pentest .

No responses yet