Software Engineering

Mastering SOLID: Building Flexible and Scalable Software

In the fast-paced world of software development, writing code that works is only half the battle. The other half is writing code that is easy to maintain, extend, and scale. This is where the SOLID principles come into play. Coined by Robert C. Martin, these five design principles serve as a cheat sheet for writing object-oriented code that remains robust even as requirements change. For intermediate to advanced developers, mastering SOLID is not just about following rules; it’s about adopting a mindset that prioritizes flexibility and clarity.

1. Single Responsibility Principle (SRP)

A class should have one, and only one, reason to change. If a class handles both business logic and data persistence, it violates SRP. Separating concerns ensures that changes to one aspect of the system don’t inadvertently break another.

// Bad: One class doing multiple jobs
public class User {
    public void save() { /* database logic */ }
    public void sendEmail() { /* email logic */ }
}

// Good: Separate responsibilities
public class UserRepository {
    public void save(User user) { /* database logic */ }
}

public class EmailService {
    public void sendWelcome(User user) { /* email logic */ }
}

2. Open/Closed Principle (OCP)

Software entities should be open for extension but closed for modification. Instead of changing existing code to add new features, you should extend it. This is often achieved through abstraction and polymorphism.

// Bad: Modifying existing code
public class Discount {
    public double calculate(Invoice invoice) {
        if (invoice.getType() == "VIP") return invoice.getTotal() * 0.9;
        // Hard to add new types without risk
    }
}

// Good: Extending via strategy pattern
public interface DiscountStrategy {
    double calculate(Invoice invoice);
}

public class VipDiscount implements DiscountStrategy {
    public double calculate(Invoice invoice) {
        return invoice.getTotal() * 0.9;
    }
}

3. Liskov Substitution Principle (LSP)

Objects of a superclass should be replaceable with objects of a subclass without breaking the application. This ensures that inheritance is used correctly and that subtypes are truly behaviors of their supertypes.

For example, a Square should not inherit from Rectangle because setting the width of a square does not make sense in the same way as for a rectangle, violating the behavioral expectations.

4. Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they do not use. It is better to have multiple specific interfaces than one general-purpose interface. This reduces coupling and makes the code easier to understand.

// Bad: Large interface
public interface Worker {
    void work();
    void eat();
    void sleep();
}

// Good: Segregated interfaces
public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

5. Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions. This promotes loose coupling and makes testing easier by allowing dependencies to be mocked or swapped out.

// Bad: Direct dependency
public class OrderService {
    private MySqlDatabase db = new MySqlDatabase();
}

// Good: Depend on abstraction
public class OrderService {
    private Database db;
    public OrderService(Database db) {
        this.db = db;
    }
}

Conclusion

Applying SOLID principles does not mean you must rigidly adhere to them in every line of code. Instead, use them as guidelines to detect when your design is becoming brittle. By focusing on Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion, you can build software that is not only functional today but also adaptable to the challenges of tomorrow.

Share: