Home SOLID Principles
Post
Cancel

SOLID Principles

Design principles and ideas are more universal and important than design patterns.


Contents


What are SOLID principles?

The SOLID principles are a set of five design principles in object-oriented programming that help developers create more maintainable, readable, and flexible software. These principles were introduced by Robert C. Martin, also known as “Uncle Bob”, and are widely accepted in software development for producing high-quality, scalable, and robust software.

By understanding and implementing these principles, developers can enhance their object-oriented design skills, leading to better software architecture and successful project outcomes.

Each letter in “SOLID” stands for a principle:

  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)

In my opinion:

  • SRP, OCP, ISP are relatively easy to understand, but harder to use in practice.
  • LSP, DIP are relatively easy to use in practice, but harder to understand.

In fact, no design principles and ideas can be followed mindlessly. The important thing is that we understand the essence of each design principle and idea and get the balance in the actual project to make the most suitable design.

Any design principles and ideas are just guidelines, not silver bullets.

Single Responsibility Principle (SRP)

Concept

  • Definition: A class should have only one reason to change, meaning it should have only one job or responsibility.
  • Purpose: This principle aims to reduce the complexity of code by ensuring that each class is focused on a single task. It makes the code more readable and easier to maintain.

More about: For SRP, How Do We Determine If a Class Has a “Single” Responsibility?

Example

Consider a class User that handles user-related data and also manages user persistence in a database.

1
2
3
4
5
6
7
8
9
public class User {
    private String name;

    public void saveToDatabase() {
        // Database code
    }

    // Other methods
}

In this example, the User class has two responsibilities: user data management and database interaction. According to SRP, these should be separated into two classes.

Open/Closed Principle (OCP)

Concept

  • Definition: Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.
  • Purpose: This principle encourages the extension of existing code behavior through inheritance, polymorphism, or other means without modifying the existing code. It helps in managing the future growth of software with minimal changes to existing code.

More about: For OCP, What Exactly Are “Extensions” and “Modifications”?

Example

Imagine you have a system where you need to apply different types of discounts to a product price. The initial implementation might look something like this:

1
2
3
4
5
6
7
8
9
10
public class DiscountService {
    public double applyDiscount(double price, String discountType) {
        if (discountType.equals("Festival")) {
            return price - price * 0.2; // 20% festival discount
        } else if (discountType.equals("Seasonal")) {
            return price - price * 0.1; // 10% seasonal discount
        }
        return price;
    }
}

In this design, whenever you want to add a new discount type, you have to modify the DiscountService class, which violates the OCP.


To make this compliant with the OCP, you can define an interface for discount strategies and implement different strategies for each discount type (Here, we use the Strategy Pattern).

Step 1: Define a Discount Strategy Interface

1
2
3
public interface DiscountStrategy {
    double applyDiscount(double price);
}

Step 2: Implement Different Discount Strategies

1
2
3
4
5
6
7
8
9
10
11
12
13
public class FestivalDiscountStrategy implements DiscountStrategy {
    @Override
    public double applyDiscount(double price) {
        return price - price * 0.2; // 20% festival discount
    }
}

public class SeasonalDiscountStrategy implements DiscountStrategy {
    @Override
    public double applyDiscount(double price) {
        return price - price * 0.1; // 10% seasonal discount
    }
}

Step 3: Modify the Discount Service to Use Discount Strategies

1
2
3
4
5
public class DiscountService {
    public double applyDiscount(double price, DiscountStrategy discountStrategy) {
        return discountStrategy.applyDiscount(price);
    }
}

Step 4: Usage Example

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
    public static void main(String[] args) {
        DiscountService discountService = new DiscountService();

        double originalPrice = 100.0;
        double festivalPrice = discountService.applyDiscount(originalPrice, new FestivalDiscountStrategy());
        double seasonalPrice = discountService.applyDiscount(originalPrice, new SeasonalDiscountStrategy());

        System.out.println("Original Price: " + originalPrice);
        System.out.println("Festival Price: " + festivalPrice);
        System.out.println("Seasonal Price: " + seasonalPrice);
    }
}

In this refactored design:

  • The DiscountService class is closed for modification because it doesn’t need to change if new discount types are introduced.
  • The system is open for extension since you can easily add new discount strategies by implementing the DiscountStrategy interface.

This approach adheres to the OCP, allowing for easier expansion and modification of discount types without altering the existing, tested code.

Liskov Substitution Principle (LSP)

Concept

  • Definition: Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.
  • Purpose: LSP ensures that a subclass can stand in for its superclass. It’s crucial for ensuring that a class hierarchy is properly designed for inheritance, promoting reusability and robustness in code.

More about: Does LSP Conflict with Polymorphism?

Example

If Bird is a superclass and Duck is a subclass, then an instance of Bird should be replaceable with Duck.

1
2
3
4
5
public class Bird {
    public void fly() { /* ... */ }
}

public class Duck extends Bird { /* ... */ }

Here, for Duck to be able to replace Bird without changing the behaviour of the program, the LSP needs to be followed. This usually means that although Duck inherits from Bird, it cannot override Bird’s concrete methods (once a concrete method is overridden, it implies a change).

Interface Segregation Principle (ISP)

Concept

  • Definition: Clients should not be forced to depend upon interfaces they do not use.
  • Purpose: This principle recommends splitting large interfaces into smaller, more specific ones so that clients only need to know about the methods that are of interest to them. It enhances the modularity of the code and reduces the impact of changes.

More about: For ISP, What Is an “Interface”?

Example

Instead of one large interface, provide multiple smaller interfaces.

Large interface (×):

1
2
3
4
public interface Worker {
    void work();
    void eat();
}

Multiple smaller interfaces (√):

1
2
3
4
5
6
7
public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

With smaller interfaces (Workable, Eatable), classes can choose to implement only the interfaces relevant to them.

Dependency Inversion Principle (DIP)

Concept

  • Definition: High-level modules should not depend on low-level modules; both should depend on abstractions. Additionally, abstractions should not depend on details; details should depend on abstractions.
  • Purpose: DIP aims to reduce the dependency among code modules, making the system more decoupled and thereby easier to refactor, change, and deploy.

More about: What gets inverted in DIP?

Example

A Book class should not directly instantiate a StandardPrinter class for printing. Instead, rely on an abstract Printer interface.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface Printer {
    void print(String text);
}

public class StandardPrinter implements Printer {
    public void print(String text) { /* ... */ }
}

public class Book {
    private Printer printer;

    public Book(Printer printer) {
        this.printer = printer;
    }

    // Other methods
}

This approach decouples the Book class from the StandardPrinter class, adhering to DIP. This improves the scalability of the code. If we need to extend the Book with other printing methods, it will be easy to do so without having to modify any existing classes.



Reference:

  • Wang, Zheng (2019) The Beauty of Design Patterns. Geek Time.
This post is licensed under CC BY 4.0 by the author.
ip