SOLID and OOP: Designing Software That Survives Change

SOLID and OOP: Designing Software That Survives Change

Table of Contents Introduction A Quick Recap of OOP Why OOP Alone Is Not Enough What is SOLID? 1. Single Responsibility Principle (SRP) 2. OpenClosed Principle (OCP) 3. Liskov Substitution Principle (LSP) 4. Interface Segregation Principle (ISP) 5. Dependency Inversion Principle (DIP) Composition Over Inheritance Common Misconceptions About SOLID When Not to Apply SOLID SOLID in a Real E-Commerce System Conclusion 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.

📰 Original Source

Read full article at Dev →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.