Post

SOLID Principles in Modern Software Development

Introduction

SOLID is an acronym for five design principles that make software designs more understandable, flexible, and maintainable. These principles were introduced by Robert C. Martin (Uncle Bob) and have become fundamental concepts in object-oriented programming and software architecture.

The SOLID principles help developers create systems that are easier to maintain and extend over time. By following these principles, you can reduce the complexity of your code, minimize dependencies, and create more modular applications.

In this comprehensive guide, we’ll explore each of the SOLID principles with detailed explanations and practical examples in multiple programming languages including C#, Python, and Java.

The Five SOLID Principles

  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)

Single Responsibility Principle (SRP)

Definition

A class should have only one reason to change, meaning it should have only one job or responsibility.

Why It Matters

When a class has multiple responsibilities, changes to one responsibility may affect or break the other responsibilities. This coupling makes the code fragile and harder to maintain.

Example: Violating SRP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// C# - Bad Example: Multiple Responsibilities
public class UserManager
{
    public void CreateUser(string username, string email)
    {
        // Validate user data
        if (string.IsNullOrEmpty(username))
            throw new ArgumentException("Username cannot be empty");
        
        // Save to database
        var connection = new SqlConnection("connectionString");
        connection.Open();
        var command = new SqlCommand($"INSERT INTO Users VALUES ('{username}', '{email}')", connection);
        command.ExecuteNonQuery();
        
        // Send welcome email
        var smtpClient = new SmtpClient("smtp.example.com");
        var mailMessage = new MailMessage("noreply@example.com", email, "Welcome", "Welcome to our platform!");
        smtpClient.Send(mailMessage);
        
        // Log the action
        File.AppendAllText("log.txt", $"User {username} created at {DateTime.Now}");
    }
}

This class has four responsibilities: validation, database operations, email sending, and logging. Any change to how we validate, store, email, or log will require modifying this class.

Example: Following SRP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// C# - Good Example: Single Responsibility
public class UserValidator
{
    public void Validate(string username, string email)
    {
        if (string.IsNullOrEmpty(username))
            throw new ArgumentException("Username cannot be empty");
        
        if (string.IsNullOrEmpty(email))
            throw new ArgumentException("Email cannot be empty");
    }
}

public class UserRepository
{
    private readonly string _connectionString;
    
    public UserRepository(string connectionString)
    {
        _connectionString = connectionString;
    }
    
    public void Save(User user)
    {
        using (var connection = new SqlConnection(_connectionString))
        {
            connection.Open();
            var command = new SqlCommand(
                "INSERT INTO Users (Username, Email) VALUES (@username, @email)", 
                connection);
            command.Parameters.AddWithValue("@username", user.Username);
            command.Parameters.AddWithValue("@email", user.Email);
            command.ExecuteNonQuery();
        }
    }
}

public class EmailService
{
    private readonly string _smtpServer;
    
    public EmailService(string smtpServer)
    {
        _smtpServer = smtpServer;
    }
    
    public void SendWelcomeEmail(string email)
    {
        var smtpClient = new SmtpClient(_smtpServer);
        var mailMessage = new MailMessage(
            "noreply@example.com", 
            email, 
            "Welcome", 
            "Welcome to our platform!");
        smtpClient.Send(mailMessage);
    }
}

public class Logger
{
    private readonly string _logPath;
    
    public Logger(string logPath)
    {
        _logPath = logPath;
    }
    
    public void Log(string message)
    {
        File.AppendAllText(_logPath, $"{DateTime.Now}: {message}\n");
    }
}

public class UserService
{
    private readonly UserValidator _validator;
    private readonly UserRepository _repository;
    private readonly EmailService _emailService;
    private readonly Logger _logger;
    
    public UserService(
        UserValidator validator,
        UserRepository repository,
        EmailService emailService,
        Logger logger)
    {
        _validator = validator;
        _repository = repository;
        _emailService = emailService;
        _logger = logger;
    }
    
    public void CreateUser(string username, string email)
    {
        _validator.Validate(username, email);
        
        var user = new User { Username = username, Email = email };
        _repository.Save(user);
        
        _emailService.SendWelcomeEmail(email);
        
        _logger.Log($"User {username} created");
    }
}

Python Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# Python - Following SRP
from abc import ABC, abstractmethod
from datetime import datetime

class UserValidator:
    def validate(self, username: str, email: str) -> None:
        if not username:
            raise ValueError("Username cannot be empty")
        if not email:
            raise ValueError("Email cannot be empty")
        if "@" not in email:
            raise ValueError("Invalid email format")

class UserRepository:
    def __init__(self, connection_string: str):
        self.connection_string = connection_string
    
    def save(self, user: dict) -> None:
        # Database operation
        print(f"Saving user {user['username']} to database")
        # Actual database code here

class EmailService:
    def __init__(self, smtp_server: str):
        self.smtp_server = smtp_server
    
    def send_welcome_email(self, email: str) -> None:
        print(f"Sending welcome email to {email}")
        # Actual email sending code here

class Logger:
    def __init__(self, log_path: str):
        self.log_path = log_path
    
    def log(self, message: str) -> None:
        with open(self.log_path, 'a') as f:
            f.write(f"{datetime.now()}: {message}\n")

class UserService:
    def __init__(
        self,
        validator: UserValidator,
        repository: UserRepository,
        email_service: EmailService,
        logger: Logger
    ):
        self.validator = validator
        self.repository = repository
        self.email_service = email_service
        self.logger = logger
    
    def create_user(self, username: str, email: str) -> None:
        self.validator.validate(username, email)
        
        user = {"username": username, "email": email}
        self.repository.save(user)
        
        self.email_service.send_welcome_email(email)
        
        self.logger.log(f"User {username} created")

Open/Closed Principle (OCP)

Definition

Software entities (classes, modules, functions) should be open for extension but closed for modification.

Why It Matters

When you need to add new functionality, you should be able to do so without modifying existing code. This reduces the risk of introducing bugs into working code and makes the system more maintainable.

Example: Violating OCP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Java - Bad Example: Not following OCP
public class PaymentProcessor {
    public void processPayment(String paymentType, double amount) {
        if (paymentType.equals("CreditCard")) {
            System.out.println("Processing credit card payment of $" + amount);
            // Credit card specific logic
        } else if (paymentType.equals("PayPal")) {
            System.out.println("Processing PayPal payment of $" + amount);
            // PayPal specific logic
        } else if (paymentType.equals("Bitcoin")) {
            System.out.println("Processing Bitcoin payment of $" + amount);
            // Bitcoin specific logic
        }
        // Need to modify this class every time we add a new payment method
    }
}

Every time we add a new payment method, we must modify the PaymentProcessor class, violating the Open/Closed Principle.

Example: Following OCP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// Java - Good Example: Following OCP
public interface PaymentMethod {
    void processPayment(double amount);
}

public class CreditCardPayment implements PaymentMethod {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing credit card payment of $" + amount);
        // Credit card specific logic
    }
}

public class PayPalPayment implements PaymentMethod {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing PayPal payment of $" + amount);
        // PayPal specific logic
    }
}

public class BitcoinPayment implements PaymentMethod {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing Bitcoin payment of $" + amount);
        // Bitcoin specific logic
    }
}

public class PaymentProcessor {
    public void processPayment(PaymentMethod paymentMethod, double amount) {
        paymentMethod.processPayment(amount);
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        PaymentProcessor processor = new PaymentProcessor();
        
        processor.processPayment(new CreditCardPayment(), 100.0);
        processor.processPayment(new PayPalPayment(), 200.0);
        processor.processPayment(new BitcoinPayment(), 300.0);
    }
}

C# Example with Strategy Pattern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// C# - Following OCP with Strategy Pattern
public interface IDiscountStrategy
{
    decimal CalculateDiscount(decimal amount);
}

public class NoDiscount : IDiscountStrategy
{
    public decimal CalculateDiscount(decimal amount)
    {
        return 0;
    }
}

public class PercentageDiscount : IDiscountStrategy
{
    private readonly decimal _percentage;
    
    public PercentageDiscount(decimal percentage)
    {
        _percentage = percentage;
    }
    
    public decimal CalculateDiscount(decimal amount)
    {
        return amount * (_percentage / 100);
    }
}

public class FixedAmountDiscount : IDiscountStrategy
{
    private readonly decimal _discountAmount;
    
    public FixedAmountDiscount(decimal discountAmount)
    {
        _discountAmount = discountAmount;
    }
    
    public decimal CalculateDiscount(decimal amount)
    {
        return Math.Min(_discountAmount, amount);
    }
}

public class SeasonalDiscount : IDiscountStrategy
{
    private readonly decimal _percentage;
    private readonly DateTime _startDate;
    private readonly DateTime _endDate;
    
    public SeasonalDiscount(decimal percentage, DateTime startDate, DateTime endDate)
    {
        _percentage = percentage;
        _startDate = startDate;
        _endDate = endDate;
    }
    
    public decimal CalculateDiscount(decimal amount)
    {
        var now = DateTime.Now;
        if (now >= _startDate && now <= _endDate)
        {
            return amount * (_percentage / 100);
        }
        return 0;
    }
}

public class ShoppingCart
{
    private readonly IDiscountStrategy _discountStrategy;
    
    public ShoppingCart(IDiscountStrategy discountStrategy)
    {
        _discountStrategy = discountStrategy;
    }
    
    public decimal CalculateTotal(decimal subtotal)
    {
        var discount = _discountStrategy.CalculateDiscount(subtotal);
        return subtotal - discount;
    }
}

Python Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# Python - Following OCP
from abc import ABC, abstractmethod
from typing import List

class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        pass

class Rectangle(Shape):
    def __init__(self, width: float, height: float):
        self.width = width
        self.height = height
    
    def area(self) -> float:
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius
    
    def area(self) -> float:
        return 3.14159 * self.radius ** 2

class Triangle(Shape):
    def __init__(self, base: float, height: float):
        self.base = base
        self.height = height
    
    def area(self) -> float:
        return 0.5 * self.base * self.height

class AreaCalculator:
    def total_area(self, shapes: List[Shape]) -> float:
        return sum(shape.area() for shape in shapes)

# Usage - adding new shapes doesn't require modifying existing code
shapes = [
    Rectangle(5, 10),
    Circle(7),
    Triangle(6, 8)
]

calculator = AreaCalculator()
total = calculator.total_area(shapes)
print(f"Total area: {total}")

Liskov Substitution Principle (LSP)

Definition

Objects of a superclass should be replaceable with objects of a subclass without breaking the application. In other words, derived classes must be substitutable for their base classes.

Why It Matters

LSP ensures that inheritance is used correctly. Violating this principle can lead to unexpected behavior and bugs that are difficult to track down.

Example: Violating LSP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Python - Bad Example: Violating LSP
class Bird:
    def fly(self):
        return "Flying in the sky"

class Sparrow(Bird):
    def fly(self):
        return "Sparrow flying"

class Ostrich(Bird):
    def fly(self):
        # Ostriches can't fly!
        raise Exception("Ostriches cannot fly")

def make_bird_fly(bird: Bird):
    print(bird.fly())

# This works fine
sparrow = Sparrow()
make_bird_fly(sparrow)

# This breaks - violates LSP
ostrich = Ostrich()
make_bird_fly(ostrich)  # Throws exception!

Example: Following LSP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# Python - Good Example: Following LSP
from abc import ABC, abstractmethod

class Bird(ABC):
    @abstractmethod
    def move(self):
        pass

class FlyingBird(Bird):
    def move(self):
        return self.fly()
    
    def fly(self):
        return "Flying in the sky"

class Sparrow(FlyingBird):
    def fly(self):
        return "Sparrow flying"

class Eagle(FlyingBird):
    def fly(self):
        return "Eagle soaring"

class WalkingBird(Bird):
    def move(self):
        return self.walk()
    
    def walk(self):
        return "Walking on ground"

class Ostrich(WalkingBird):
    def walk(self):
        return "Ostrich running on ground"

class Penguin(WalkingBird):
    def walk(self):
        return "Penguin waddling"

def make_bird_move(bird: Bird):
    print(bird.move())

# Now all birds work correctly
sparrow = Sparrow()
ostrich = Ostrich()
penguin = Penguin()

make_bird_move(sparrow)   # Sparrow flying
make_bird_move(ostrich)   # Ostrich running on ground
make_bird_move(penguin)   # Penguin waddling

C# Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// C# - Following LSP
public abstract class Account
{
    protected decimal _balance;
    
    public decimal Balance => _balance;
    
    public virtual void Deposit(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Amount must be positive");
        
        _balance += amount;
    }
    
    public abstract bool CanWithdraw(decimal amount);
    
    public virtual void Withdraw(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Amount must be positive");
        
        if (!CanWithdraw(amount))
            throw new InvalidOperationException("Insufficient funds");
        
        _balance -= amount;
    }
}

public class SavingsAccount : Account
{
    private const decimal MinimumBalance = 100;
    
    public override bool CanWithdraw(decimal amount)
    {
        return _balance - amount >= MinimumBalance;
    }
}

public class CheckingAccount : Account
{
    private decimal _overdraftLimit = 500;
    
    public override bool CanWithdraw(decimal amount)
    {
        return _balance + _overdraftLimit >= amount;
    }
}

public class FixedDepositAccount : Account
{
    private readonly DateTime _maturityDate;
    
    public FixedDepositAccount(DateTime maturityDate)
    {
        _maturityDate = maturityDate;
    }
    
    public override bool CanWithdraw(decimal amount)
    {
        return DateTime.Now >= _maturityDate && _balance >= amount;
    }
}

// Usage - all account types can be used interchangeably
public class BankingService
{
    public void ProcessWithdrawal(Account account, decimal amount)
    {
        if (account.CanWithdraw(amount))
        {
            account.Withdraw(amount);
            Console.WriteLine($"Withdrawal successful. New balance: {account.Balance}");
        }
        else
        {
            Console.WriteLine("Withdrawal not allowed");
        }
    }
}

Java Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// Java - Following LSP
public abstract class Vehicle {
    protected String name;
    protected int speed;
    
    public Vehicle(String name) {
        this.name = name;
        this.speed = 0;
    }
    
    public abstract void accelerate(int increment);
    public abstract void brake(int decrement);
    
    public int getSpeed() {
        return speed;
    }
    
    public String getName() {
        return name;
    }
}

public class Car extends Vehicle {
    private static final int MAX_SPEED = 200;
    
    public Car(String name) {
        super(name);
    }
    
    @Override
    public void accelerate(int increment) {
        speed = Math.min(speed + increment, MAX_SPEED);
    }
    
    @Override
    public void brake(int decrement) {
        speed = Math.max(speed - decrement, 0);
    }
}

public class Bicycle extends Vehicle {
    private static final int MAX_SPEED = 50;
    
    public Bicycle(String name) {
        super(name);
    }
    
    @Override
    public void accelerate(int increment) {
        speed = Math.min(speed + increment, MAX_SPEED);
    }
    
    @Override
    public void brake(int decrement) {
        speed = Math.max(speed - decrement, 0);
    }
}

public class TrafficSimulator {
    public void simulateTraffic(Vehicle vehicle) {
        System.out.println(vehicle.getName() + " starting simulation");
        vehicle.accelerate(30);
        System.out.println("Current speed: " + vehicle.getSpeed());
        vehicle.brake(10);
        System.out.println("After braking: " + vehicle.getSpeed());
    }
    
    public static void main(String[] args) {
        TrafficSimulator simulator = new TrafficSimulator();
        
        // Both vehicle types work correctly
        simulator.simulateTraffic(new Car("Tesla"));
        simulator.simulateTraffic(new Bicycle("Mountain Bike"));
    }
}

Interface Segregation Principle (ISP)

Definition

Clients should not be forced to depend on interfaces they do not use. It’s better to have many specific interfaces than one general-purpose interface.

Why It Matters

Large interfaces force classes to implement methods they don’t need, leading to unnecessary coupling and making the code harder to maintain.

Example: Violating ISP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// C# - Bad Example: Violating ISP
public interface IWorker
{
    void Work();
    void Eat();
    void Sleep();
    void GetSalary();
}

public class HumanWorker : IWorker
{
    public void Work()
    {
        Console.WriteLine("Human working");
    }
    
    public void Eat()
    {
        Console.WriteLine("Human eating");
    }
    
    public void Sleep()
    {
        Console.WriteLine("Human sleeping");
    }
    
    public void GetSalary()
    {
        Console.WriteLine("Human getting salary");
    }
}

public class RobotWorker : IWorker
{
    public void Work()
    {
        Console.WriteLine("Robot working");
    }
    
    // Robots don't eat or sleep!
    public void Eat()
    {
        throw new NotImplementedException();
    }
    
    public void Sleep()
    {
        throw new NotImplementedException();
    }
    
    public void GetSalary()
    {
        throw new NotImplementedException();
    }
}

Example: Following ISP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// C# - Good Example: Following ISP
public interface IWorkable
{
    void Work();
}

public interface IFeedable
{
    void Eat();
}

public interface ISleepable
{
    void Sleep();
}

public interface IPayable
{
    void GetSalary();
}

public class HumanWorker : IWorkable, IFeedable, ISleepable, IPayable
{
    public void Work()
    {
        Console.WriteLine("Human working");
    }
    
    public void Eat()
    {
        Console.WriteLine("Human eating");
    }
    
    public void Sleep()
    {
        Console.WriteLine("Human sleeping");
    }
    
    public void GetSalary()
    {
        Console.WriteLine("Human getting salary");
    }
}

public class RobotWorker : IWorkable
{
    public void Work()
    {
        Console.WriteLine("Robot working");
    }
}

public class ContractorWorker : IWorkable, IPayable
{
    public void Work()
    {
        Console.WriteLine("Contractor working");
    }
    
    public void GetSalary()
    {
        Console.WriteLine("Contractor getting payment");
    }
}

// Usage
public class WorkManager
{
    public void ManageWork(IWorkable worker)
    {
        worker.Work();
    }
    
    public void ManagePayroll(IPayable employee)
    {
        employee.GetSalary();
    }
}

Python Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# Python - Following ISP
from abc import ABC, abstractmethod

class Printer(ABC):
    @abstractmethod
    def print_document(self, document: str) -> None:
        pass

class Scanner(ABC):
    @abstractmethod
    def scan_document(self) -> str:
        pass

class Fax(ABC):
    @abstractmethod
    def fax_document(self, document: str) -> None:
        pass

class SimplePrinter(Printer):
    def print_document(self, document: str) -> None:
        print(f"Printing: {document}")

class MultiFunctionPrinter(Printer, Scanner, Fax):
    def print_document(self, document: str) -> None:
        print(f"Multi-function printer printing: {document}")
    
    def scan_document(self) -> str:
        return "Scanned document content"
    
    def fax_document(self, document: str) -> None:
        print(f"Faxing: {document}")

class ModernPrinter(Printer, Scanner):
    def print_document(self, document: str) -> None:
        print(f"Modern printer printing: {document}")
    
    def scan_document(self) -> str:
        return "Scanned document content"

# Usage
def print_documents(printer: Printer, documents: list):
    for doc in documents:
        printer.print_document(doc)

def scan_and_print(device: Scanner & Printer, document_count: int):
    for i in range(document_count):
        scanned = device.scan_document()
        device.print_document(scanned)

Java Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// Java - Following ISP
public interface Readable {
    String read();
}

public interface Writable {
    void write(String data);
}

public interface Closeable {
    void close();
}

public class File implements Readable, Writable, Closeable {
    private String content = "";
    private boolean closed = false;
    
    @Override
    public String read() {
        if (closed) {
            throw new IllegalStateException("File is closed");
        }
        return content;
    }
    
    @Override
    public void write(String data) {
        if (closed) {
            throw new IllegalStateException("File is closed");
        }
        content += data;
    }
    
    @Override
    public void close() {
        closed = true;
    }
}

public class ReadOnlyFile implements Readable, Closeable {
    private final String content;
    private boolean closed = false;
    
    public ReadOnlyFile(String content) {
        this.content = content;
    }
    
    @Override
    public String read() {
        if (closed) {
            throw new IllegalStateException("File is closed");
        }
        return content;
    }
    
    @Override
    public void close() {
        closed = true;
    }
}

public class WriteOnlyFile implements Writable, Closeable {
    private String content = "";
    private boolean closed = false;
    
    @Override
    public void write(String data) {
        if (closed) {
            throw new IllegalStateException("File is closed");
        }
        content += data;
    }
    
    @Override
    public void close() {
        closed = true;
    }
}

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.

Why It Matters

DIP reduces coupling between different parts of the system, making it more flexible and easier to modify. It allows you to change implementation details without affecting high-level business logic.

Example: Violating DIP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Java - Bad Example: Violating DIP
public class MySQLDatabase {
    public void save(String data) {
        System.out.println("Saving to MySQL: " + data);
    }
}

public class UserService {
    private MySQLDatabase database;
    
    public UserService() {
        this.database = new MySQLDatabase();
    }
    
    public void createUser(String username) {
        // Business logic
        database.save(username);
    }
}

The UserService is tightly coupled to MySQLDatabase. If we want to switch to PostgreSQL or MongoDB, we need to modify UserService.

Example: Following DIP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// Java - Good Example: Following DIP
public interface Database {
    void save(String data);
    String retrieve(String id);
}

public class MySQLDatabase implements Database {
    @Override
    public void save(String data) {
        System.out.println("Saving to MySQL: " + data);
    }
    
    @Override
    public String retrieve(String id) {
        return "Data from MySQL with id: " + id;
    }
}

public class PostgreSQLDatabase implements Database {
    @Override
    public void save(String data) {
        System.out.println("Saving to PostgreSQL: " + data);
    }
    
    @Override
    public String retrieve(String id) {
        return "Data from PostgreSQL with id: " + id;
    }
}

public class MongoDBDatabase implements Database {
    @Override
    public void save(String data) {
        System.out.println("Saving to MongoDB: " + data);
    }
    
    @Override
    public String retrieve(String id) {
        return "Data from MongoDB with id: " + id;
    }
}

public class UserService {
    private Database database;
    
    public UserService(Database database) {
        this.database = database;
    }
    
    public void createUser(String username) {
        // Business logic
        database.save(username);
    }
    
    public String getUser(String id) {
        return database.retrieve(id);
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        // Easy to switch between different databases
        Database mysqlDb = new MySQLDatabase();
        UserService userService1 = new UserService(mysqlDb);
        userService1.createUser("John");
        
        Database postgresDb = new PostgreSQLDatabase();
        UserService userService2 = new UserService(postgresDb);
        userService2.createUser("Jane");
        
        Database mongoDb = new MongoDBDatabase();
        UserService userService3 = new UserService(mongoDb);
        userService3.createUser("Bob");
    }
}

C# Example with Dependency Injection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// C# - Following DIP with Dependency Injection
public interface ILogger
{
    void Log(string message);
}

public interface IEmailSender
{
    void SendEmail(string to, string subject, string body);
}

public interface INotificationService
{
    void SendNotification(string userId, string message);
}

// Implementations
public class FileLogger : ILogger
{
    private readonly string _filePath;
    
    public FileLogger(string filePath)
    {
        _filePath = filePath;
    }
    
    public void Log(string message)
    {
        File.AppendAllText(_filePath, $"{DateTime.Now}: {message}\n");
    }
}

public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine($"{DateTime.Now}: {message}");
    }
}

public class SmtpEmailSender : IEmailSender
{
    private readonly string _smtpServer;
    
    public SmtpEmailSender(string smtpServer)
    {
        _smtpServer = smtpServer;
    }
    
    public void SendEmail(string to, string subject, string body)
    {
        Console.WriteLine($"Sending email via SMTP to {to}: {subject}");
    }
}

public class SendGridEmailSender : IEmailSender
{
    private readonly string _apiKey;
    
    public SendGridEmailSender(string apiKey)
    {
        _apiKey = apiKey;
    }
    
    public void SendEmail(string to, string subject, string body)
    {
        Console.WriteLine($"Sending email via SendGrid to {to}: {subject}");
    }
}

public class PushNotificationService : INotificationService
{
    public void SendNotification(string userId, string message)
    {
        Console.WriteLine($"Sending push notification to {userId}: {message}");
    }
}

public class SmsNotificationService : INotificationService
{
    public void SendNotification(string userId, string message)
    {
        Console.WriteLine($"Sending SMS to {userId}: {message}");
    }
}

// High-level module
public class OrderService
{
    private readonly ILogger _logger;
    private readonly IEmailSender _emailSender;
    private readonly INotificationService _notificationService;
    
    public OrderService(
        ILogger logger,
        IEmailSender emailSender,
        INotificationService notificationService)
    {
        _logger = logger;
        _emailSender = emailSender;
        _notificationService = notificationService;
    }
    
    public void PlaceOrder(string userId, string productId)
    {
        _logger.Log($"Order placed by {userId} for product {productId}");
        
        _emailSender.SendEmail(
            $"{userId}@example.com",
            "Order Confirmation",
            $"Your order for {productId} has been placed");
        
        _notificationService.SendNotification(
            userId,
            $"Your order for {productId} is confirmed");
    }
}

// Usage with Dependency Injection
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Configure dependencies
        services.AddSingleton<ILogger>(new FileLogger("log.txt"));
        services.AddSingleton<IEmailSender>(new SendGridEmailSender("api-key"));
        services.AddSingleton<INotificationService>(new PushNotificationService());
        services.AddTransient<OrderService>();
    }
}

Python Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# Python - Following DIP
from abc import ABC, abstractmethod
from typing import List

class MessageSender(ABC):
    @abstractmethod
    def send(self, recipient: str, message: str) -> None:
        pass

class EmailSender(MessageSender):
    def send(self, recipient: str, message: str) -> None:
        print(f"Sending email to {recipient}: {message}")

class SmsSender(MessageSender):
    def send(self, recipient: str, message: str) -> None:
        print(f"Sending SMS to {recipient}: {message}")

class SlackSender(MessageSender):
    def send(self, recipient: str, message: str) -> None:
        print(f"Sending Slack message to {recipient}: {message}")

class DataStore(ABC):
    @abstractmethod
    def save(self, key: str, value: str) -> None:
        pass
    
    @abstractmethod
    def retrieve(self, key: str) -> str:
        pass

class RedisStore(DataStore):
    def __init__(self):
        self.data = {}
    
    def save(self, key: str, value: str) -> None:
        print(f"Saving to Redis: {key}")
        self.data[key] = value
    
    def retrieve(self, key: str) -> str:
        return self.data.get(key, "")

class PostgresStore(DataStore):
    def __init__(self):
        self.data = {}
    
    def save(self, key: str, value: str) -> None:
        print(f"Saving to PostgreSQL: {key}")
        self.data[key] = value
    
    def retrieve(self, key: str) -> str:
        return self.data.get(key, "")

class NotificationService:
    def __init__(
        self,
        message_sender: MessageSender,
        data_store: DataStore
    ):
        self.message_sender = message_sender
        self.data_store = data_store
    
    def notify_user(self, user_id: str, message: str) -> None:
        # Store notification
        self.data_store.save(f"notification_{user_id}", message)
        
        # Retrieve user contact
        contact = self.data_store.retrieve(f"user_{user_id}")
        
        # Send notification
        self.message_sender.send(contact, message)

# Usage - easy to swap implementations
email_service = NotificationService(
    EmailSender(),
    RedisStore()
)
email_service.notify_user("user123", "Your order has been shipped")

sms_service = NotificationService(
    SmsSender(),
    PostgresStore()
)
sms_service.notify_user("user456", "Your order has been delivered")

slack_service = NotificationService(
    SlackSender(),
    RedisStore()
)
slack_service.notify_user("user789", "New message from support")

Combining SOLID Principles

Real-World Example: E-Commerce System

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
// C# - Complete Example Combining All SOLID Principles

// Interfaces (ISP)
public interface IProductRepository
{
    Product GetById(int id);
    void Save(Product product);
}

public interface IOrderRepository
{
    Order GetById(int id);
    void Save(Order order);
}

public interface IPaymentProcessor
{
    PaymentResult Process(decimal amount, PaymentDetails details);
}

public interface INotificationSender
{
    void Send(string recipient, string message);
}

public interface ILogger
{
    void Log(string message);
}

// DIP - Abstractions for different payment methods
public interface IPaymentMethod
{
    string Name { get; }
    PaymentResult ProcessPayment(decimal amount, PaymentDetails details);
}

// OCP - Easy to extend with new payment methods
public class CreditCardPayment : IPaymentMethod
{
    public string Name => "Credit Card";
    
    public PaymentResult ProcessPayment(decimal amount, PaymentDetails details)
    {
        // Credit card specific processing
        return new PaymentResult { Success = true, TransactionId = Guid.NewGuid().ToString() };
    }
}

public class PayPalPayment : IPaymentMethod
{
    public string Name => "PayPal";
    
    public PaymentResult ProcessPayment(decimal amount, PaymentDetails details)
    {
        // PayPal specific processing
        return new PaymentResult { Success = true, TransactionId = Guid.NewGuid().ToString() };
    }
}

public class CryptoPayment : IPaymentMethod
{
    public string Name => "Cryptocurrency";
    
    public PaymentResult ProcessPayment(decimal amount, PaymentDetails details)
    {
        // Crypto specific processing
        return new PaymentResult { Success = true, TransactionId = Guid.NewGuid().ToString() };
    }
}

// SRP - Each class has single responsibility
public class OrderValidator
{
    public ValidationResult Validate(Order order)
    {
        var errors = new List<string>();
        
        if (order.Items == null || order.Items.Count == 0)
            errors.Add("Order must contain at least one item");
        
        if (order.CustomerId <= 0)
            errors.Add("Invalid customer ID");
        
        if (order.TotalAmount <= 0)
            errors.Add("Order total must be positive");
        
        return new ValidationResult
        {
            IsValid = errors.Count == 0,
            Errors = errors
        };
    }
}

public class OrderCalculator
{
    public decimal CalculateTotal(Order order)
    {
        decimal subtotal = order.Items.Sum(item => item.Price * item.Quantity);
        decimal tax = subtotal * 0.1m;
        decimal shipping = CalculateShipping(order);
        
        return subtotal + tax + shipping;
    }
    
    private decimal CalculateShipping(Order order)
    {
        // Shipping calculation logic
        return order.Items.Sum(item => item.Weight) * 0.5m;
    }
}

// SRP - Single responsibility for payment processing
public class PaymentProcessor : IPaymentProcessor
{
    private readonly ILogger _logger;
    
    public PaymentProcessor(ILogger logger)
    {
        _logger = logger;
    }
    
    public PaymentResult Process(decimal amount, PaymentDetails details)
    {
        _logger.Log($"Processing payment of {amount}");
        
        IPaymentMethod paymentMethod = GetPaymentMethod(details.Method);
        var result = paymentMethod.ProcessPayment(amount, details);
        
        _logger.Log($"Payment result: {result.Success}");
        
        return result;
    }
    
    private IPaymentMethod GetPaymentMethod(string method)
    {
        return method switch
        {
            "CreditCard" => new CreditCardPayment(),
            "PayPal" => new PayPalPayment(),
            "Crypto" => new CryptoPayment(),
            _ => throw new NotSupportedException($"Payment method {method} not supported")
        };
    }
}

// SRP - Single responsibility for order processing
public class OrderService
{
    private readonly IOrderRepository _orderRepository;
    private readonly IProductRepository _productRepository;
    private readonly IPaymentProcessor _paymentProcessor;
    private readonly INotificationSender _notificationSender;
    private readonly OrderValidator _validator;
    private readonly OrderCalculator _calculator;
    private readonly ILogger _logger;
    
    public OrderService(
        IOrderRepository orderRepository,
        IProductRepository productRepository,
        IPaymentProcessor paymentProcessor,
        INotificationSender notificationSender,
        OrderValidator validator,
        OrderCalculator calculator,
        ILogger logger)
    {
        _orderRepository = orderRepository;
        _productRepository = productRepository;
        _paymentProcessor = paymentProcessor;
        _notificationSender = notificationSender;
        _validator = validator;
        _calculator = calculator;
        _logger = logger;
    }
    
    public OrderResult PlaceOrder(Order order)
    {
        // Validate order
        var validationResult = _validator.Validate(order);
        if (!validationResult.IsValid)
        {
            return new OrderResult
            {
                Success = false,
                Errors = validationResult.Errors
            };
        }
        
        // Calculate total
        order.TotalAmount = _calculator.CalculateTotal(order);
        
        // Process payment
        var paymentResult = _paymentProcessor.Process(
            order.TotalAmount,
            order.PaymentDetails);
        
        if (!paymentResult.Success)
        {
            _logger.Log($"Payment failed for order {order.Id}");
            return new OrderResult
            {
                Success = false,
                Errors = new List<string> { "Payment processing failed" }
            };
        }
        
        // Save order
        order.Status = "Confirmed";
        order.TransactionId = paymentResult.TransactionId;
        _orderRepository.Save(order);
        
        // Send notification
        _notificationSender.Send(
            order.CustomerEmail,
            $"Order {order.Id} confirmed. Transaction: {paymentResult.TransactionId}");
        
        _logger.Log($"Order {order.Id} placed successfully");
        
        return new OrderResult
        {
            Success = true,
            OrderId = order.Id,
            TransactionId = paymentResult.TransactionId
        };
    }
}

// Models
public class Order
{
    public int Id { get; set; }
    public int CustomerId { get; set; }
    public string CustomerEmail { get; set; }
    public List<OrderItem> Items { get; set; }
    public decimal TotalAmount { get; set; }
    public string Status { get; set; }
    public PaymentDetails PaymentDetails { get; set; }
    public string TransactionId { get; set; }
}

public class OrderItem
{
    public int ProductId { get; set; }
    public string ProductName { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
    public decimal Weight { get; set; }
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int Stock { get; set; }
}

public class PaymentDetails
{
    public string Method { get; set; }
    public string CardNumber { get; set; }
    public string Email { get; set; }
    public string WalletAddress { get; set; }
}

public class PaymentResult
{
    public bool Success { get; set; }
    public string TransactionId { get; set; }
    public string ErrorMessage { get; set; }
}

public class OrderResult
{
    public bool Success { get; set; }
    public int OrderId { get; set; }
    public string TransactionId { get; set; }
    public List<string> Errors { get; set; }
}

public class ValidationResult
{
    public bool IsValid { get; set; }
    public List<string> Errors { get; set; }
}

Benefits of Following SOLID Principles

Maintainability

Code that follows SOLID principles is easier to understand and modify. Each class has a clear purpose, and changes are localized to specific areas.

Testability

SOLID code is highly testable because:

  • Dependencies are injected, making it easy to use mocks and stubs
  • Each class has a single responsibility, making unit tests focused
  • Interfaces allow for easy substitution of implementations

Flexibility

SOLID principles make your code more flexible:

  • New features can be added without modifying existing code (OCP)
  • Components can be easily swapped (DIP)
  • Different implementations can be used in different contexts (LSP)

Reduced Coupling

By depending on abstractions rather than concrete implementations, SOLID reduces coupling between components, making the system more modular.

Code Reusability

Well-designed SOLID code is more reusable because:

  • Single-purpose classes can be used in different contexts
  • Interfaces define clear contracts
  • Implementations are interchangeable

Common Pitfalls and How to Avoid Them

Over-Engineering

Don’t apply SOLID principles blindly. Consider:

  • Project size and complexity
  • Team experience
  • Time constraints
  • Actual requirements

Start simple and refactor toward SOLID as needs emerge.

Premature Abstraction

Don’t create interfaces and abstractions “just in case.” Wait until you have:

  • At least two implementations
  • A clear need for flexibility
  • Actual variation in behavior

Ignoring Context

SOLID principles are guidelines, not laws. Consider:

  • Performance requirements
  • Team preferences
  • Project constraints
  • Domain complexity

Analysis Paralysis

Don’t spend excessive time designing the perfect architecture. Instead:

  • Start with working code
  • Refactor iteratively
  • Apply principles as patterns emerge
  • Focus on actual pain points

Practical Guidelines for Implementation

Start with SRP

Focus on Single Responsibility Principle first:

  1. Identify classes with multiple responsibilities
  2. Extract separate classes for each responsibility
  3. Use meaningful names that reflect the single purpose

Apply OCP Through Abstraction

Make your code open for extension:

  1. Identify areas likely to change
  2. Create abstractions (interfaces/abstract classes)
  3. Use composition over inheritance
  4. Favor strategy and factory patterns

Ensure LSP with Proper Inheritance

Maintain substitutability:

  1. Test derived classes where base classes are expected
  2. Ensure derived classes don’t strengthen preconditions
  3. Ensure derived classes don’t weaken postconditions
  4. Consider composition over inheritance

Use ISP to Create Focused Interfaces

Keep interfaces cohesive:

  1. Start with larger interfaces
  2. Split when clients use only subsets
  3. Group related methods
  4. Use interface composition

Implement DIP with Dependency Injection

Invert dependencies:

  1. Identify high-level and low-level modules
  2. Create abstractions between them
  3. Use constructor injection
  4. Consider using DI containers

Testing SOLID Code

Unit Testing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# Python - Unit Testing SOLID Code
import unittest
from unittest.mock import Mock, MagicMock

class TestOrderService(unittest.TestCase):
    def setUp(self):
        self.order_repository = Mock()
        self.product_repository = Mock()
        self.payment_processor = Mock()
        self.notification_sender = Mock()
        self.validator = Mock()
        self.calculator = Mock()
        self.logger = Mock()
        
        self.order_service = OrderService(
            self.order_repository,
            self.product_repository,
            self.payment_processor,
            self.notification_sender,
            self.validator,
            self.calculator,
            self.logger
        )
    
    def test_place_order_with_valid_order(self):
        # Arrange
        order = Order(id=1, customer_id=100)
        self.validator.validate.return_value = ValidationResult(is_valid=True)
        self.calculator.calculate_total.return_value = 100.0
        self.payment_processor.process.return_value = PaymentResult(
            success=True,
            transaction_id="TXN123"
        )
        
        # Act
        result = self.order_service.place_order(order)
        
        # Assert
        self.assertTrue(result.success)
        self.assertEqual(result.transaction_id, "TXN123")
        self.order_repository.save.assert_called_once()
        self.notification_sender.send.assert_called_once()
    
    def test_place_order_with_invalid_order(self):
        # Arrange
        order = Order(id=1, customer_id=0)
        self.validator.validate.return_value = ValidationResult(
            is_valid=False,
            errors=["Invalid customer ID"]
        )
        
        # Act
        result = self.order_service.place_order(order)
        
        # Assert
        self.assertFalse(result.success)
        self.assertEqual(result.errors, ["Invalid customer ID"])
        self.payment_processor.process.assert_not_called()

Conclusion

SOLID principles are fundamental guidelines for creating maintainable, flexible, and robust software systems. While they originated in object-oriented programming, their core concepts apply to software design in general.

Key takeaways:

  • SRP: Each class should have one reason to change
  • OCP: Extend behavior without modifying existing code
  • LSP: Derived classes must be substitutable for base classes
  • ISP: Create focused, specific interfaces
  • DIP: Depend on abstractions, not implementations

Remember that SOLID principles are guidelines, not rigid rules. Apply them judiciously based on your specific context, and always prioritize practical solutions over theoretical perfection.

References

  1. Martin, Robert C. “Clean Code: A Handbook of Agile Software Craftsmanship.” Prentice Hall, 2008.
  2. Martin, Robert C. “Agile Software Development, Principles, Patterns, and Practices.” Pearson, 2002.
  3. Gamma, Erich, et al. “Design Patterns: Elements of Reusable Object-Oriented Software.” Addison-Wesley, 1994.
  4. Fowler, Martin. “Refactoring: Improving the Design of Existing Code.” Addison-Wesley, 2018.
  5. Freeman, Eric, et al. “Head First Design Patterns.” O’Reilly Media, 2004.
  6. Martin, Robert C. “The Clean Coder: A Code of Conduct for Professional Programmers.” Prentice Hall, 2011.
  7. Evans, Eric. “Domain-Driven Design: Tackling Complexity in the Heart of Software.” Addison-Wesley, 2003.
  8. Shore, James. “The Art of Agile Development.” O’Reilly Media, 2007.
  9. Seemann, Mark. “Dependency Injection in .NET.” Manning Publications, 2011.
  10. Uncle Bob’s Blog: https://blog.cleancoder.com/
This post is licensed under CC BY 4.0 by the author.