DEV Community

Cover image for SOLID and OOP: Designing Software That Survives Change
Phùng Hữu Đại Khánh
Phùng Hữu Đại Khánh

Posted on

SOLID and OOP: Designing Software That Survives Change

Table of Contents


Introduction

Imagine you're building an application using Object-Oriented Programming (OOP).

At first, everything seems great:

  • The code is clean.
  • Features work correctly.
  • Development moves quickly.

Then new requirements arrive.

Suddenly:

  • Fixing one bug breaks another feature.
  • A single class handles business logic, database access, and email notifications.
  • Adding a new feature requires modifying multiple existing classes.
  • Testing becomes increasingly difficult.

If this sounds familiar, you've encountered one of the biggest challenges in software development:

Managing change.

This is exactly the problem SOLID was designed to address.


A Quick Recap of OOP

Object-Oriented Programming is built on four fundamental concepts.

  • Encapsulation: Hide internal state and expose behavior through controlled interfaces.

  • Abstraction: Expose only what is necessary while hiding implementation details.

  • Inheritance: Reuse behavior through parent-child relationships.

  • Polymorphism: Allow different objects to respond to the same message in different ways.


Why OOP Alone Is Not Enough

OOP provides powerful tools.

However, tools alone do not guarantee good design.

Poorly designed OOP systems often suffer from:

  • God Classes
  • Tight Coupling
  • Fragile Inheritance Hierarchies
  • Difficult Testing
  • Low Maintainability
  • Poor Extensibility

SOLID provides guidelines for using OOP effectively.


What is SOLID?

SOLID is a collection of five design principles popularized by Robert C. Martin (Uncle Bob).

  • S — Single Responsibility Principle
  • O — Open/Closed Principle
  • L — Liskov Substitution Principle
  • I — Interface Segregation Principle
  • D — Dependency Inversion Principle

The goal is not perfect design.

The goal is software that remains easy to evolve over time.


1. Single Responsibility Principle (SRP)

A class should have one and only one reason to change.

Relationship to OOP

SRP complements Encapsulation by encouraging related behaviors to stay together while separating unrelated concerns.

It is also closely related to:

  • Cohesion
  • Separation of Concerns

A highly cohesive class typically has a single responsibility.

Common Misconception

A responsibility is not a method.

This is perfectly acceptable:

class UserValidator {

    public boolean validateEmail(String email) {}
    public boolean validatePhone(String phone) {}
    public boolean validateName(String name) {}
}
Enter fullscreen mode Exit fullscreen mode

All methods belong to the same responsibility:

Validation

SRP Violation

class User {

    public void validate() {}
    public void saveToDatabase() {}
    public void sendWelcomeEmail() {}
}
Enter fullscreen mode Exit fullscreen mode

This class has multiple reasons to change.

Benefits

  • Higher cohesion
  • Easier maintenance
  • Better testing
  • Reduced coupling

2. Open/Closed Principle (OCP)

Software entities should be open for extension but closed for modification.

Relationship to OOP

OCP relies heavily on:

  • Abstraction
  • Polymorphism

OCP Violation

class PaymentProcessor {

    public void process(String type) {

        if(type.equals("credit")) {

        } else if(type.equals("paypal")) {

        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Every new payment method requires modifying existing code.

Better Design

interface PaymentMethod {
    void process();
}

class CreditCardPayment implements PaymentMethod {
    public void process() {}
}

class PayPalPayment implements PaymentMethod {
    public void process() {}
}
Enter fullscreen mode Exit fullscreen mode

OCP Is Not About Interfaces

Many developers think:

OCP = Create an Interface

Not necessarily.

Interfaces are only one implementation technique.

Other approaches include:

  • Strategy Pattern
  • Plugin Architecture
  • Event-Driven Systems
  • Dependency Injection
  • Configuration-Based Extensions

The real goal is:

Extend behavior without modifying stable code.


3. Liskov Substitution Principle (LSP)

Subtypes must be substitutable for their base types.

Relationship to OOP

LSP protects the correct use of:

  • Inheritance
  • Polymorphism

Example

interface Bird {
    void eat();
    void fly();
}

class Sparrow implements Bird {
    public void eat() {}
    public void fly() {}
}

class Ostrich implements Bird {

    public void eat() {}

    public void fly() {
        throw new UnsupportedOperationException();
    }
}
Enter fullscreen mode Exit fullscreen mode

The abstraction promises that all birds can fly.

Ostrich breaks that promise.

Better Design

interface Bird {
    void eat();
}

interface FlyingBird extends Bird {
    void fly();
}
Enter fullscreen mode Exit fullscreen mode

ISP vs LSP

Although often discussed together, they solve different problems.

Principle Focus
ISP Interface Design
LSP Substitutability

LSP focuses on behavioral compatibility between abstractions and implementations.


4. Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they do not use.

ISP Violation

interface Worker {
    void work();
    void eat();
}

class Robot implements Worker {

    public void work() {}

    public void eat() {
        throw new UnsupportedOperationException();
    }
}
Enter fullscreen mode Exit fullscreen mode

The interface is too broad.

Better Design

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}
Enter fullscreen mode Exit fullscreen mode

Benefits

  • Smaller contracts
  • Lower coupling
  • Easier maintenance

5. Dependency Inversion Principle (DIP)

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

DIP Violation

class NotificationService {

    private EmailSender sender = new EmailSender();
}
Enter fullscreen mode Exit fullscreen mode

Business logic depends directly on implementation details.

Better Design

interface MessageSender {
    void send(String message);
}

class EmailSender implements MessageSender {}

class NotificationService {

    private MessageSender sender;

    public NotificationService(MessageSender sender) {
        this.sender = sender;
    }
}
Enter fullscreen mode Exit fullscreen mode

DIP vs Dependency Injection

A common misconception is:

DIP and DI are the same thing.

They are not.

DIP is a design principle.

DI is a technique used to implement that principle.

DIP answers:

What should depend on what?

DI answers:

How are dependencies provided?

The true goal of DIP is to ensure that business policies do not depend on implementation details.


Composition Over Inheritance

A principle commonly associated with SOLID is:

Favor Composition Over Inheritance

Example

interface NotificationSender {
    void send(String message);
}

class EmailSender implements NotificationSender {}

class SmsSender implements NotificationSender {}

class NotificationService {

    private final NotificationSender sender;

    public NotificationService(NotificationSender sender) {
        this.sender = sender;
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice that NotificationService does not inherit from EmailSender or SmsSender.

Instead, it composes behavior through abstraction.

Benefits:

  • More flexibility
  • Better testability
  • Lower coupling
  • Stronger support for OCP and DIP

Common Misconceptions About SOLID

Misconception #1

"Every class must have one method."

False.

SRP is about responsibilities, not method count.

Misconception #2

"OCP means creating interfaces."

False.

Interfaces are only one possible implementation.

Misconception #3

"DIP equals Dependency Injection."

False.

DI is a technique.

DIP is the principle.

Misconception #4

"SOLID should always be applied everywhere."

False.

Over-engineering is a real problem.


When Not to Apply SOLID

Not every project needs enterprise-level architecture.

For small applications:

interface UserValidator
class UserValidatorImpl
Enter fullscreen mode Exit fullscreen mode

may add unnecessary complexity.

Apply SOLID when it solves a real problem.

Avoid applying it purely for theoretical purity.


SOLID in a Real E-Commerce System

SRP

Separate:

  • Order Processing
  • Persistence
  • Notification

OCP

Add new payment methods without modifying existing services.

LSP

Allow specialized order types to safely replace base order types.

ISP

Create focused contracts:

  • Payable
  • Refundable
  • Shippable

DIP

Depend on abstractions rather than specific database implementations.


Conclusion

At its core, SOLID is about managing change.

As software evolves, requirements change, business rules shift, and new features emerge.

Poor design makes these changes expensive and risky.

Each SOLID principle addresses a different source of rigidity:

  • SRP reduces unrelated changes.
  • OCP minimizes modifications to stable code.
  • LSP protects substitutability.
  • ISP avoids unnecessary dependencies.
  • DIP separates business rules from implementation details.

Together, these principles help developers build systems that can evolve without becoming increasingly difficult to maintain.

The goal is not to eliminate change.

The goal is to make change less expensive.

Top comments (0)