跳转至

SOLID Principles: Comprehensive Knowledge Guide

Overview

SOLID is an acronym representing five fundamental design principles in object-oriented programming and software engineering. These principles, introduced by Robert C. Martin, help create maintainable, scalable, and robust software systems. The five principles are:

  1. Single Responsibility Principle (SRP)
  2. Open/Closed Principle (OCP)
  3. Liskov Substitution Principle (LSP)
  4. Interface Segregation Principle (ISP)
  5. Dependency Inversion Principle (DIP)

1. Single Responsibility Principle (SRP)

Definition

Each class should have a single, well-defined responsibility. A class should have only one reason to change, meaning it should handle only one aspect of the application's functionality.

Core Concepts

  • Single Concern: A class should focus on doing one thing and doing it well
  • Separation of Concerns: Different responsibilities should be separated into different classes
  • Cohesion: Classes with a single responsibility tend to have higher cohesion (internal consistency)

Problems Without SRP

When classes have multiple responsibilities:

  • Poor Separation of Concerns: Multiple unrelated functionalities are mixed together
  • Low Coherence: The class lacks internal consistency and clear purpose
  • Difficult to Fragment and Reuse: Cannot extract and reuse individual functionalities in other projects
  • Harder to Test: Testing becomes complex when a single class handles multiple concerns
  • Increased Coupling: Multiple responsibilities lead to increased dependencies and coupling

Example: Violation of SRP

public class WordProcessor {
    public void saveDocument(Document doc) {
        // File handling logic
    }

    public Document loadDocument(String fileName) {
        // File loading logic
        return null;
    }

    public void spellCheckDocument(Document doc) {
        // Spell checking logic
    }
}

In this example, the WordProcessor class has three responsibilities: 1. Saving documents (file persistence) 2. Loading documents (file persistence) 3. Spell checking (document analysis)

Example: SRP Compliant Design

public class FileHandler {
    public void saveDocument(Document doc, String format) {
        // Only handles file persistence
    }

    public Document loadDocument(String fileName) {
        // Only handles file loading
        return null;
    }
}

public class SpellChecker {
    public void spellCheckDocument(Document doc, String lang) {
        // Only handles spell checking
    }
}

Benefits of SRP

  • Easier Collaboration: Multiple developers can work on different components without conflict
  • Code Reusability: Modules with single responsibilities can be easily shared among projects
  • Improved Readability: Classes are easier to understand when they have a clear, focused purpose
  • Better Testability: Single-responsibility classes are simpler to unit test
  • Reduced Coupling: Changes to one responsibility don't cascade to others
  • Maintainability: Bugs are localized and easier to fix
  • Flexibility: New requirements can be addressed without modifying existing classes

2. Open/Closed Principle (OCP)

Definition

Software classes should be closed for modification but open for extension. This means you should be able to add new functionality without changing existing code.

Core Philosophy

The principle was introduced by Bertrand Meyer and addresses the challenge of creating stable, extensible systems:

  • Closed for Modification: Existing, tested code should remain unchanged to avoid introducing bugs
  • Open for Extension: New functionality should be added through extension mechanisms without modifying the original code
  • Stability with Flexibility: Balances the need for stable code with the ability to accommodate new requirements

Implementation Strategies

1. Interface-Based Design

Define class behavior through abstract interface definitions:

public interface ICardPaymentProvider {
    CardPaymentResponseCode takePaymentFromCard(String cardName, 
                                               String cardNumber, 
                                               Date start, Date end,
                                               String cvv, 
                                               String postcode);
}

public abstract class CardPaymentProviderBase implements ICardPaymentProvider {
    public final CardPaymentResponseCode takePaymentFromCard(String cardName, 
                                                            String cardNumber,
                                                            Date start, Date end,
                                                            String cvv, 
                                                            String postcode) {
        // Closed for modification - contains critical validation logic
        boolean numOK = isCardNumValid(cardNumber);
        if (!numOK) {
            return CardPaymentResponseCode.INVALID_CARDNUM;
        }

        // Delegates to overrideable method for extension
        return onTakePayment();
    }

    protected CardPaymentResponseCode onTakePayment() {
        // Open for extension - subclasses override this method
        return CardPaymentResponseCode.PAYMENT_PROVIDER_UNIMPLEMENTED;
    }
}

2. Dynamic Polymorphism

Runtime type checking allows the same code to work with different implementations:

public interface Printable {
    void doPrint();
}

class Circle implements Printable {
    public void doPrint() {
        // Circle-specific printing implementation
    }
}

class Rectangle implements Printable {
    public void doPrint() {
        // Rectangle-specific printing implementation
    }
}

// Use polymorphism - implementation determined at runtime
Printable shape = new Circle();
shape.doPrint(); // Calls Circle's implementation

3. Static Polymorphism (Generics/Templates)

Compile-time type checking with flexible type parameters:

public class Stack<E> {
    private E[] stackData;

    public Stack() {
        stackData = (E[]) new Object[MAX_SIZE];
    }

    public void push(E element) {
        // Implementation
    }

    public E pop() {
        // Implementation
        return null;
    }
}

// Type is fixed at compile time
Stack<Integer> intStack = new Stack<>();
Stack<String> stringStack = new Stack<>();

Type Erasure: During compilation, generic type parameters are erased and replaced with Object: - Stack<Integer> becomes Stack with Object[] internally - Type safety is checked at compile-time but erased at runtime - Restrictions: Cannot create instances like new E() or new E[]

4. The final Keyword

Prevents modification at the code level:

// Final class - cannot be subclassed
public final class ImmutablePaymentProvider {
    // ...
}

// Final method - cannot be overridden
public final boolean makePayment(String number) {
    // Implementation
}

// Final attribute - cannot be changed
public static final double PI = 3.1415926;

5. Protected Methods for Extension

Non-final protected methods allow extension while maintaining stability:

public class PaypalPaymentProvider extends CardPaymentProviderBase {
    protected CardPaymentResponseCode onTakePayment() {
        // This method is open for extension
        System.out.println("Attempting to take payment from PayPal");
        // Implementation specific to PayPal
        return CardPaymentResponseCode.OK;
    }
}

Practical Example: Shape Calculation System

Problem: Create a system that can calculate areas in different units (meters squared, millimeters squared) without modifying the scaling code when new shapes are added.

// Interface defines contract
public interface IShape {
    double calculateArea();
}

// Abstract base class provides stable interface
public abstract class Shape implements IShape {
    protected abstract double calculateArea();

    public final double getArea() {
        // Closed for modification - stable scaling logic here
        return calculateArea();
    }
}

// Concrete implementations - open for extension
public final class Circle extends Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    protected final double calculateArea() {
        return radius * radius * Math.PI;
    }
}

public final class Rectangle extends Shape {
    private double length;
    private double width;

    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    protected final double calculateArea() {
        return length * width;
    }
}

// Usage
IShape shape1 = new Rectangle(40, 50);
IShape shape2 = new Circle(200);
System.out.println("Area of shape1 is " + shape1.getArea());
System.out.println("Area of shape2 is " + shape2.getArea());

Facade Pattern with Package Access

The Facade pattern works with OCP by encapsulating implementation details:

// All internal classes use default (package-private) access
class InternalClass1 {
    // Hidden from external use
}

class InternalClass2 {
    // Hidden from external use
}

// Only the facade is public
public class PublicFacade {
    public void publicMethod() {
        // Coordinates internal classes
    }
}

Benefits: - Internal implementation can change without affecting external clients - Only the public interface needs to remain stable - Package acts as a single unit that can be extended without modification

Design by Contract

Formal specifications of class behavior that should not be violated:

// Contract: if you set focus1 and focus2 to different points,
// they should remain different when retrieved
public class Ellipse {
    private Point focus1, focus2;

    public void setFocus1(Point focus) {
        this.focus1 = focus;
    }

    public void setFocus2(Point focus) {
        this.focus2 = focus;
    }

    public Point getFocus1() {
        return focus1;
    }

    public Point getFocus2() {
        return focus2;
    }
}

Implementation Checklist for OCP

  • Define clear interface definitions (public contracts)
  • Use the final keyword on methods that should not be modified
  • Provide protected overrideable methods as extension points
  • Isolate modification-prone code from extension code
  • Use package-private access for internal implementation
  • Create abstract base classes with template methods
  • Apply appropriate access modifiers (public, protected, private)

3. Liskov Substitution Principle (LSP)

Definition

Derived classes (subclasses) must be substitutable for their base classes without breaking the application. If a derived class violates the base class's contract, it violates LSP.

Core Concept

Substitutability: Any code written to work with a base class should work correctly with any of its subclasses, without knowing which subclass is actually being used.

If S is a subtype of T, then objects of type S may be substituted 
for objects of type T without altering any of the desirable properties 
of that program.

Example: LSP Violation

public class Ellipse {
    protected Point focus1, focus2;

    public Point getFocus1() {
        return focus1;
    }

    public void setFocus1(Point focus1) {
        this.focus1 = focus1;
    }

    public Point getFocus2() {
        return focus2;
    }

    public void setFocus2(Point focus2) {
        this.focus2 = focus2;
    }
}

// Violation: Circle breaks Ellipse's contract
public class Circle extends Ellipse {
    public void setFocus1(Point focus) {
        // A circle has only one focus point, so set both
        this.focus1 = focus;
        this.focus2 = focus;  // Violates the implicit contract!
    }

    public void setFocus2(Point focus) {
        // Same violation
        this.focus1 = focus;
        this.focus2 = focus;
    }
}

LSP Violation Detection

private static void checkShape(Ellipse e) {
    Point a = new Point(-1, 0);
    Point b = new Point(1, 0);

    e.setFocus1(a);
    e.setFocus2(b);

    // These assertions should pass for any Ellipse
    assert(e.getFocus1().equals(a));  // Fails if e is a Circle!
    assert(e.getFocus2().equals(b));  // Fails if e is a Circle!
}

The problem occurs because Circle violates the implicit contract of Ellipse. When you set two different focus points on an Ellipse, they should remain different. A Circle cannot maintain this contract because a circle has only one focus point (the center).

Design by Contract

Definition: Each method has an implicit or explicit contract specifying: - Preconditions: What conditions must be true before calling the method - Postconditions: What conditions will be true after the method executes - Invariants: What conditions remain true throughout the method's execution

LSP Requirement: Subclasses must not weaken preconditions or postconditions

Practical Solutions to LSP Violations

1. Use the final Keyword

Prevents problematic method overriding:

public class Ellipse {
    protected Point focus1, focus2;

    public final void setFocus1(Point focus1) {
        this.focus1 = focus1;
    }

    public final void setFocus2(Point focus2) {
        this.focus2 = focus2;
    }
}

// Now Circle cannot override these methods

2. Correct the Inheritance Hierarchy

Use composition instead of inheritance if true substitutability doesn't exist:

public interface Shape {
    double getArea();
}

public class Ellipse implements Shape {
    private Point focus1, focus2;

    public double getArea() {
        // Calculate ellipse area
        return 0;
    }

    public void setFocus1(Point focus) {
        this.focus1 = focus;
    }

    public void setFocus2(Point focus) {
        this.focus2 = focus;
    }
}

public class Circle implements Shape {
    private Point center;
    private double radius;

    public double getArea() {
        return radius * radius * Math.PI;
    }

    // No inheritance relationship - no contract violation
}

3. Extract Common Interface

If classes share some behavior but not all:

public interface GeometricShape {
    double getArea();
}

public class Circle implements GeometricShape {
    private double radius;

    public double getArea() {
        return radius * radius * Math.PI;
    }
}

public class Rectangle implements GeometricShape {
    private double length, width;

    public double getArea() {
        return length * width;
    }
}

LSP and Inheritance Rules

When to Use Inheritance (to maintain LSP): - True is-a relationship exists - Subclass can fulfill all contracts of the base class - Example: Doctor is-a Person, Apple is-a Fruit

When NOT to Use Inheritance: - Only need to reuse functionality without a true is-a relationship - Example: Person is-NOT-an EncryptionHelper - Solution: Use composition instead

Benefits of Following LSP

  • Predictability: Code behaves consistently with base class expectations
  • Reliability: Substitution doesn't introduce unexpected bugs
  • Maintainability: Inheritance hierarchies remain coherent and logical
  • Extensibility: Safe to add new subclasses without breaking existing code
  • Testing: Can write tests for base classes that work for all subclasses

4. Interface Segregation Principle (ISP)

Definition

No client should be forced to depend on methods it does not use. Create fine-grained, client-specific interfaces rather than one large, general-purpose interface.

Core Philosophy

  • Small, Focused Interfaces: Interfaces should be designed around specific client needs
  • Avoid Fat Interfaces: Large interfaces with many methods can force clients to implement unused methods
  • Client-Specific Abstractions: Different clients have different needs; provide interfaces tailored to those needs

Problems with Fat Interfaces

// BAD: One large interface for SQL operations
public interface ISQLHelper {
    boolean insert(String table, Map<String, Object> data);
    boolean update(String table, Map<String, Object> data);
    boolean delete(String table, String condition);
    ResultSet select(String query);
    boolean createTable(String schema);
    boolean dropTable(String tableName);
}

// A client that only needs to insert data is forced to implement
// all methods, even those it doesn't use
public class InsertOnlyClient implements ISQLHelper {
    public boolean insert(String table, Map<String, Object> data) {
        // Actual implementation
        return true;
    }

    public boolean update(String table, Map<String, Object> data) {
        throw new UnsupportedOperationException();
    }

    public boolean delete(String table, String condition) {
        throw new UnsupportedOperationException();
    }

    public ResultSet select(String query) {
        throw new UnsupportedOperationException();
    }

    public boolean createTable(String schema) {
        throw new UnsupportedOperationException();
    }

    public boolean dropTable(String tableName) {
        throw new UnsupportedOperationException();
    }
}

ISP Compliant Design

// GOOD: Segregated interfaces for specific clients

public interface IInsertHelper {
    boolean insert(String table, Map<String, Object> data);
}

public interface IUpdateHelper {
    boolean update(String table, Map<String, Object> data);
}

public interface IDeleteHelper {
    boolean delete(String table, String condition);
}

public interface ISelectHelper {
    ResultSet select(String query);
}

public interface ISchemaHelper {
    boolean createTable(String schema);
    boolean dropTable(String tableName);
}

// Clients implement only the interfaces they need
public class InsertClient implements IInsertHelper {
    public boolean insert(String table, Map<String, Object> data) {
        // Implementation
        return true;
    }
}

public class SelectClient implements ISelectHelper {
    public ResultSet select(String query) {
        // Implementation
        return null;
    }
}

public class FullDatabaseManager implements IInsertHelper, IUpdateHelper, 
                                           IDeleteHelper, ISelectHelper,
                                           ISchemaHelper {
    // Implements all interfaces - has full functionality
}

Real-World Example: Document Processing System

// Segregated interfaces for different document operations

public interface IRenderable {
    void render();
}

public interface IPDFExportable {
    byte[] exportToPDF();
}

public interface IShapeProvider {
    Shape getShape();
}

// Different document types implement only needed interfaces

public class Circle implements IShapeProvider, IRenderable {
    public Shape getShape() {
        // Return circle shape
        return null;
    }

    public void render() {
        // Render circle
    }
}

public class Image implements IRenderable, IPDFExportable {
    public void render() {
        // Render image
    }

    public byte[] exportToPDF() {
        // Export to PDF
        return null;
    }
}

public class Text implements IRenderable, IPDFExportable {
    public void render() {
        // Render text
    }

    public byte[] exportToPDF() {
        // Export to PDF
        return null;
    }
}

public class Rectangle implements IShapeProvider, IRenderable {
    public Shape getShape() {
        // Return rectangle shape
        return null;
    }

    public void render() {
        // Render rectangle
    }
}

Benefits of ISP

  • Reduced Coupling: Clients depend only on the methods they use
  • Flexibility: Easy to add new implementations with specific interfaces
  • Cleaner Implementations: No need for empty/stub method implementations
  • Better Testability: Easier to create test doubles with minimal interface
  • Single Responsibility: Interfaces have focused responsibilities
  • Evolutionary Design: Easy to modify interfaces without breaking all clients

ISP Implementation Checklist

  • Analyze client needs and group them logically
  • Create focused interfaces for each group
  • Avoid large, general-purpose interfaces
  • Use interface composition when multiple behaviors are needed
  • Review existing implementations for unused methods (indicator of ISP violation)
  • Consider the actual usage patterns of classes

5. Dependency Inversion Principle (DIP)

Definition

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

Core Philosophy

  • Depend on Abstractions, Not Concretions: Use interfaces and abstract classes rather than concrete implementations
  • Reduce Coupling: High-level logic should not be tightly coupled to implementation details
  • Enable Flexibility: Easy to swap implementations without changing high-level code
  • Inversion of Control: The dependency flow is inverted from the traditional top-down approach

Traditional (Incorrect) Dependency Flow

High-Level Module (Business Logic)
Low-Level Module (Database, File System)

In this traditional approach, high-level modules directly depend on low-level modules, creating tight coupling.

Inverted Dependency Flow

High-Level Module (Business Logic)
    Abstraction (Interface)
Low-Level Module (Implementation)

Both high-level and low-level modules depend on the abstraction, not on each other.

Example: Violation of DIP

// BAD: High-level module depends directly on low-level module
public class UserService {
    private MySQLDatabase database;

    public UserService() {
        this.database = new MySQLDatabase();  // Direct dependency on concrete class
    }

    public void saveUser(User user) {
        database.save(user);
    }
}

public class MySQLDatabase {
    public void save(User user) {
        // MySQL-specific saving logic
    }
}

Problems: - UserService is tightly coupled to MySQLDatabase - Changing database technology requires modifying UserService - Cannot use different database implementations without code changes - Difficult to test (cannot use mock database)

DIP Compliant Design

// Interface defines abstraction
public interface IDatabase {
    void save(User user);
    User getUserById(int id);
    void delete(int id);
}

// Low-level module implements the interface
public class MySQLDatabase implements IDatabase {
    public void save(User user) {
        // MySQL-specific implementation
    }

    public User getUserById(int id) {
        // MySQL-specific implementation
        return null;
    }

    public void delete(int id) {
        // MySQL-specific implementation
    }
}

// Alternative implementation
public class PostgreSQLDatabase implements IDatabase {
    public void save(User user) {
        // PostgreSQL-specific implementation
    }

    public User getUserById(int id) {
        // PostgreSQL-specific implementation
        return null;
    }

    public void delete(int id) {
        // PostgreSQL-specific implementation
    }
}

// High-level module depends on abstraction
public class UserService {
    private IDatabase database;

    // Dependency injection - provides flexibility
    public UserService(IDatabase database) {
        this.database = database;
    }

    public void saveUser(User user) {
        database.save(user);
    }

    public User getUser(int id) {
        return database.getUserById(id);
    }

    public void deleteUser(int id) {
        database.delete(id);
    }
}

// Usage - can easily swap implementations
public class Application {
    public static void main(String[] args) {
        // Use MySQL
        IDatabase mysqlDb = new MySQLDatabase();
        UserService serviceWithMySQL = new UserService(mysqlDb);

        // Use PostgreSQL - same service, different implementation
        IDatabase pgDb = new PostgreSQLDatabase();
        UserService serviceWithPostgreSQL = new UserService(pgDb);

        // Use mock for testing
        IDatabase mockDb = new MockDatabase();
        UserService serviceForTesting = new UserService(mockDb);
    }
}

public class MockDatabase implements IDatabase {
    public void save(User user) {
        // Mock implementation for testing
    }

    public User getUserById(int id) {
        // Mock implementation
        return new User(id, "Test User");
    }

    public void delete(int id) {
        // Mock implementation
    }
}

Dependency Injection

A key technique for implementing DIP:

Constructor Injection

public class UserService {
    private IDatabase database;

    // Dependency provided through constructor
    public UserService(IDatabase database) {
        this.database = database;
    }
}

Setter Injection

public class UserService {
    private IDatabase database;

    // Dependency provided through setter
    public void setDatabase(IDatabase database) {
        this.database = database;
    }
}

Interface Injection

public interface IDatabaseInjectable {
    void injectDatabase(IDatabase database);
}

public class UserService implements IDatabaseInjectable {
    private IDatabase database;

    public void injectDatabase(IDatabase database) {
        this.database = database;
    }
}

Advanced Example: Multi-Service System

// Abstractions (interfaces)
public interface IAuthentication {
    boolean authenticate(String username, String password);
}

public interface IAuthorization {
    boolean isAuthorized(User user, String action);
}

public interface IEmailService {
    void sendEmail(String to, String subject, String body);
}

// Concrete implementations
public class ActiveDirectoryAuth implements IAuthentication {
    public boolean authenticate(String username, String password) {
        // Active Directory authentication logic
        return true;
    }
}

public class RoleBasedAuthorization implements IAuthorization {
    public boolean isAuthorized(User user, String action) {
        // Role-based authorization logic
        return user.getRole().canPerform(action);
    }
}

public class SMTPEmailService implements IEmailService {
    public void sendEmail(String to, String subject, String body) {
        // SMTP email sending logic
    }
}

// High-level service depends on abstractions
public class UserManagementService {
    private IAuthentication authentication;
    private IAuthorization authorization;
    private IEmailService emailService;

    public UserManagementService(IAuthentication auth, 
                                 IAuthorization authz,
                                 IEmailService email) {
        this.authentication = auth;
        this.authorization = authz;
        this.emailService = email;
    }

    public boolean createUser(String username, String password, User newUser) {
        if (!authentication.authenticate(username, password)) {
            return false;
        }

        if (!authorization.isAuthorized(new User(username, null), "create_user")) {
            return false;
        }

        // Create user logic...

        emailService.sendEmail(newUser.getEmail(), "Welcome", 
                             "Your account has been created");

        return true;
    }
}

Benefits of DIP

  • Loose Coupling: High-level modules are not dependent on low-level implementation details
  • Flexibility: Easy to swap implementations without changing business logic
  • Testability: Easy to create mock implementations for unit testing
  • Maintainability: Changes to implementations don't affect high-level code
  • Scalability: Can extend functionality by providing new implementations
  • Reusability: High-level modules can be reused with different implementations

DIP Implementation Checklist

  • Identify dependencies in high-level modules
  • Create interfaces for each dependency
  • Make high-level modules depend on interfaces, not concrete classes
  • Implement dependency injection to provide concrete implementations
  • Avoid direct instantiation of dependencies (use constructor/setter injection)
  • Use factory patterns or service locators for complex object creation
  • Ensure concrete implementations are only created at the application boundaries

Relationships Between SOLID Principles

The five SOLID principles work together to create well-designed object-oriented systems:

Principle Focuses On Key Benefit
SRP Class responsibilities Single reason to change; better testability
OCP Code extension Stable, existing code remains untouched
LSP Inheritance contracts Predictable substitutability
ISP Interface granularity Clients depend only on needed methods
DIP Dependency direction Depend on abstractions, not concretions

Hierarchical Application

  1. Start with DIP: Design your system using abstractions (interfaces)
  2. Apply SRP: Each class should have one responsibility
  3. Apply OCP: Make classes open for extension, closed for modification
  4. Apply ISP: Ensure interfaces are focused and client-specific
  5. Respect LSP: Maintain proper inheritance hierarchies

Common Anti-Patterns and Solutions

Rigid Design

Problem: Hard to modify functionality without major rework (e.g., hardcoded values)

Example:

// BAD
String yes = "Yes";
String no = "No";

Solution:

// GOOD
String yes = Strings.getInstance().getString("YES");
String no = Strings.getInstance().getString("NO");

Fragility

Problem: High probability of errors after modification

Example:

// BAD - Magic numbers
if (c >= 65 && c <= 90)
    c = c + 32;

Solution:

// GOOD - Meaningful constants
if (c >= 'A' && c <= 'Z')
    c = c + ('a' - 'A');

Immobility

Problem: Code is difficult to reuse in other projects

Solution: Apply SRP and reduce coupling through proper abstraction

Viscosity of Design

Problem: Difficult to maintain the original design philosophy when making changes

Solution: Enforce design principles consistently; use code reviews

Data Duplication

Problem: Storing the same data in multiple places (violates DRY principle)

Example:

// BAD - Balance calculated and stored separately
double balance = 1000;
// Later: transactions modify the stored balance incorrectly

Solution:

// GOOD - Calculate balance from transaction history
double balance = calculateBalance(); // SELECT SUM(amount) FROM transactions


Implementation Best Practices

1. Use Access Modifiers Correctly

public class MyClass {
    // Private: internal implementation details
    private int internalState;

    // Protected: extension points for subclasses
    protected void extensionPoint() {
        // Can be overridden
    }

    // Public: stable interface
    public final void publicAPI() {
        // Cannot be overridden - maintains contract
    }
}

2. Prefer Composition Over Inheritance

// BAD - Violates LSP and creates unnecessary coupling
class Person extends EncryptionHelper {
    // Person is not-an EncryptionHelper
}

// GOOD - Composition preserves single responsibility
class Person {
    private EncryptionHelper encrypter = new EncryptionHelper();

    public String encryptData(String data) {
        return encrypter.encrypt(data);
    }
}

3. Use Interfaces for Abstraction

// Define services through interfaces
public interface IPaymentProcessor {
    PaymentResult processPayment(PaymentDetails details);
}

// Implement for different providers
public class CreditCardProcessor implements IPaymentProcessor {
    // Implementation
}

public class PayPalProcessor implements IPaymentProcessor {
    // Implementation
}

// High-level code depends on interface
public class CheckoutService {
    private IPaymentProcessor paymentProcessor;

    public CheckoutService(IPaymentProcessor processor) {
        this.paymentProcessor = processor;
    }
}

4. Apply Template Method Pattern for OCP

public abstract class DataProcessor {
    // Template method - closed for modification
    public final void process(String data) {
        String validated = validate(data);
        String processed = transform(validated);
        save(processed);
    }

    // Extension points - open for extension
    protected abstract String validate(String data);
    protected abstract String transform(String data);
    protected abstract void save(String data);
}

public class CSVProcessor extends DataProcessor {
    protected String validate(String data) {
        // CSV-specific validation
        return data;
    }

    protected String transform(String data) {
        // CSV-specific transformation
        return data;
    }

    protected void save(String data) {
        // CSV-specific saving
    }
}

5. Use Enum for Type-Safe Constants

public enum PaymentStatus {
    OK,
    INVALID_CARD_NUMBER,
    INVALID_EXPIRY_DATE,
    INSUFFICIENT_FUNDS,
    PROVIDER_ERROR;
}

public class PaymentResult {
    private PaymentStatus status;

    public PaymentResult(PaymentStatus status) {
        this.status = status;
    }

    public boolean isSuccess() {
        return status == PaymentStatus.OK;
    }
}

Conclusion

The SOLID principles provide a comprehensive framework for writing high-quality, maintainable object-oriented software. They work together to:

  • Reduce coupling between modules
  • Improve code flexibility and extensibility
  • Make systems easier to test and maintain
  • Facilitate team collaboration
  • Create systems that can evolve with changing requirements
  • Prevent common design problems (rigidity, fragility, immobility)

Applying these principles requires practice and experience, but the long-term benefits in code quality, maintainability, and team productivity make the effort worthwhile. Remember that these are guidelines, not rigid rules—apply them judiciously based on your specific context and requirements.