DEV Community

Cover image for Understanding SOLID Principles in Software Design
Bellamer
Bellamer

Posted on • Edited on

Understanding SOLID Principles in Software Design

The SOLID principles are a set of guidelines that help software developers design robust, scalable, and maintainable systems. These principles were introduced by Robert C. Martin (Uncle Bob) and are essential in object-oriented programming to create flexible and reusable code.

In this post, we’ll dive into each SOLID principle, explain its purpose, and provide examples in Java to demonstrate their application.

1. Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change. This means a class should have only one job or responsibility.

Why SRP Matters

When a class has multiple responsibilities, changes to one responsibility may affect or break other parts of the code. By adhering to SRP, we ensure better maintainability and testability.

Example

// Violating SRP: A class that handles both user authentication and database operations.
class UserManager {
    public void authenticateUser(String username, String password) {
        // Authentication logic
    }

    public void saveUserToDatabase(User user) {
        // Database logic
    }
}

// Following SRP: Separate responsibilities into distinct classes.
class AuthService {
    public void authenticateUser(String username, String password) {
        // Authentication logic
    }
}

class UserRepository {
    public void saveUserToDatabase(User user) {
        // Database logic
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, AuthService handles authentication, and UserRepository manages database operations. Each class has a single responsibility, making the code cleaner and more modular.

2. Open/Closed Principle (OCP)

Definition: Classes should be open for extension but closed for modification. This means you should be able to add new functionality without altering existing code.

Why OCP Matters

When you modify existing code, you risk introducing bugs. OCP promotes extending functionality through inheritance or composition rather than altering the original implementation.

Example

// Violating OCP: Adding a new discount type requires modifying the existing code.
class DiscountCalculator {
    public double calculateDiscount(String discountType, double amount) {
        if ("NEWYEAR".equals(discountType)) {
            return amount * 0.10;
        } else if ("BLACKFRIDAY".equals(discountType)) {
            return amount * 0.20;
        }
        return 0;
    }
}

// Following OCP: Use polymorphism to add new discount types without changing existing code.
interface Discount {
    double apply(double amount);
}

class NewYearDiscount implements Discount {
    public double apply(double amount) {
        return amount * 0.10;
    }
}

class BlackFridayDiscount implements Discount {
    public double apply(double amount) {
        return amount * 0.20;
    }
}

class DiscountCalculator {
    public double calculateDiscount(Discount discount, double amount) {
        return discount.apply(amount);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, adding a new discount type simply requires creating a new class implementing the Discount interface.

3. Liskov Substitution Principle (LSP)

Definition: Subtypes must be substitutable for their base types without altering the correctness of the program.

Why LSP Matters

Violating LSP can lead to unexpected behavior and errors when using polymorphism. Derived classes must honor the contract defined by their base classes.

Example

// Violating LSP: A subclass changes the behavior of the parent class in an unexpected way.
class Bird {
    public void fly() {
        System.out.println("Flying...");
    }
}

class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Penguins can't fly!");
    }
}

// Following LSP: Refactor the hierarchy to honor substitutability.
abstract class Bird {
    public abstract void move();
}

class FlyingBird extends Bird {
    public void move() {
        System.out.println("Flying...");
    }
}

class Penguin extends Bird {
    public void move() {
        System.out.println("Swimming...");
    }
}
Enter fullscreen mode Exit fullscreen mode

By redesigning the hierarchy, both FlyingBird and Penguin behave correctly when substituted for Bird.

4. Interface Segregation Principle (ISP)

Definition: Clients should not be forced to implement interfaces they don’t use. Instead, create smaller, more specific interfaces.

Why ISP Matters

Large interfaces force implementing classes to include methods they don’t need. This results in bloated code and unnecessary dependencies.

Example

// Violating ISP: A large interface with unrelated methods.
interface Worker {
    void work();
    void eat();
}

class Robot implements Worker {
    public void work() {
        System.out.println("Working...");
    }

    public void eat() {
        // Robots don't eat, but they're forced to implement this method.
        throw new UnsupportedOperationException("Robots don't eat!");
    }
}

// Following ISP: Split the interface into smaller, focused interfaces.
interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class Robot implements Workable {
    public void work() {
        System.out.println("Working...");
    }
}

class Human implements Workable, Eatable {
    public void work() {
        System.out.println("Working...");
    }

    public void eat() {
        System.out.println("Eating...");
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, Robot only implements the Workable interface, avoiding unnecessary methods.

5. Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.

Why DIP Matters

Direct dependencies on concrete implementations make code rigid and hard to test. DIP promotes the use of abstractions (interfaces) to decouple components.

Example

// Violating DIP: High-level class depends on a low-level implementation.
class MySQLDatabase {
    public void connect() {
        System.out.println("Connecting to MySQL...");
    }
}

class UserService {
    private MySQLDatabase database;

    public UserService() {
        this.database = new MySQLDatabase(); // Tight coupling
    }

    public void performDatabaseOperation() {
        database.connect();
    }
}

// Following DIP: High-level class depends on an abstraction.
interface Database {
    void connect();
}

class MySQLDatabase implements Database {
    public void connect() {
        System.out.println("Connecting to MySQL...");
    }
}

class UserService {
    private Database database;

    public UserService(Database database) {
        this.database = database; // Depend on abstraction
    }

    public void performDatabaseOperation() {
        database.connect();
    }
}

// Usage
Database db = new MySQLDatabase();
UserService userService = new UserService(db);
userService.performDatabaseOperation();
Enter fullscreen mode Exit fullscreen mode

With this design, you can easily swap the Database implementation (e.g., PostgreSQL, MongoDB) without modifying the UserService class.

Conclusion

The SOLID principles are powerful tools for creating maintainable, scalable, and robust software. Here’s a quick recap:

  1. SRP: One class, one responsibility.
  2. OCP: Extend functionality without modifying existing code.
  3. LSP: Subtypes must be substitutable for their base types.
  4. ISP: Prefer smaller, focused interfaces.
  5. DIP: Depend on abstractions, not concrete implementations.

By applying these principles, your code will be easier to understand, test, and adapt to changing requirements. Start small, refactor as needed, and gradually incorporate these principles into your development process!

Top comments (8)

Collapse
 
fejan profile image
Fejan

Very useful article

Collapse
 
miroslav_hubenko_03721d2d profile image
Miroslav Hubenko

Very useful article. There are a lot of them, of course, but it's good that I came across this one. It's very good that there is a "what does this mean" block. Thank you.

Question: (refers to principle 4) what to do if the language does not have an implementation of multiple inheritance?

Collapse
 
be11amer profile image
Bellamer

Do you mean “multiple inheritance” ?
For example languages like Java, where only single inheritance is allowed, you can use multiple interfaces to ensure that your classes only implement the methods they actually need. This avoids the problem of forcing clients to depend on methods they don’t use and also implements all necessary interfaces.
Let take another example, a device either only implement printable or both printable and scannable. So you have two types of printer: SimplePrinter and MultiFunctionPrinter.

// ISP: Split large interfaces into smaller ones
interface Printable {
    void printDocument(String content);
}

interface Scannable {
    void scanDocument();
}

// Classes implement only the required interfaces
class SimplePrinter implements Printable {
    @Override
    public void printDocument(String content) {
        System.out.println("Printing: " + content);
    }
}

class MultiFunctionDevice implements Printable, Scannable {
    @Override
    public void printDocument(String content) {
        System.out.println("Printing: " + content);
    }

    @Override
    public void scanDocument() {
        System.out.println("Scanning document...");
    }
}
Enter fullscreen mode Exit fullscreen mode

Hope I get your question right :)

Collapse
 
miroslav_hubenko_03721d2d profile image
Miroslav Hubenko

Thank you. I understand the logic. I just decided to discuss. Let's take JavaScript for example. If you don't use typscript, there is a completely different approach to working with classes. There are no interfaces there at all. I wanted to ask if the principles are adapted for languages that are not OOP or not fully OOP? I have generally heard that using straight OOP and all the principles from Uncle Bob is no longer adequate from the point of view of real development in terms of man-hours spent as well as the final speed of the code.

Thread Thread
 
zorqie profile image
I, Pavlov

Round is still the preferred shape for most wheels, no matter what the cool FP kids say.

These are guiding principles, not laws. Breaking them won't send you to programmers jail but may lead to refactoring hell.

Thread Thread
 
luis_domingues_423ec81a56 profile image
Luis Domingues

Image description

Collapse
 
jottyjohn profile image
Jotty John

Nice Article.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.