DEV Community

Cover image for Understanding SOLID Principles: A Developer's Guide with Examples
Outdated Dev
Outdated Dev

Posted on • Edited on

Understanding SOLID Principles: A Developer's Guide with Examples

Welcome to another deep dive into software engineering fundamentals! Today, we're exploring the SOLID principles - five design principles that help us write more maintainable, scalable, and robust code. Whether you're working with C#, Python, TypeScript or any other object-oriented programming language, these principles are universally applicable.

What are SOLID Principles?

SOLID is an acronym representing five object-oriented design principles introduced by Robert C. Martin (aka Uncle Bob). These principles help developers create code that is:

  • Maintainable: Easy to modify and extend
  • Testable: Simple to unit test
  • Reusable: Can be used in different contexts
  • Understandable: Clear and readable

Let's break down each principle with practical examples.


S - Single Responsibility Principle (SRP)

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

Each class should have a single, well-defined responsibility. If a class handles multiple concerns, it becomes harder to maintain and test.

❌ Violation Example

C#

public class UserManager
{
    public void CreateUser(string username, string email)
    {
        // Validate user data
        if (string.IsNullOrWhitespace(username) || string.IsNullOrWhitespace(email))
            throw new ArgumentException("Username and email are required");


        // Save to database
        // ... database code ...

        // Send welcome email
        // ... email code ...
    }
}
Enter fullscreen mode Exit fullscreen mode

Python

class UserManager:
    def create_user(self, username: str, email: str):
        # Validate user data
        if not username or not email:
            raise ValueError("Username and email are required")

        # Save to database
        # ... database code ...

        # Send welcome email
        # ... email code ...
Enter fullscreen mode Exit fullscreen mode

TypeScript

class UserManager {
    createUser(username: string, email: string): void {
        // Validate user data
        if (!username || !email) {
            throw new Error("Username and email are required");
        }

        // Save to database
        // ... database code ...

        // Send welcome email
        // ... email code ...
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ Correct Implementation

C#

// Separate responsibilities into different classes
public class UserValidator
{
    public void Validate(string username, string email)
    {
        if (string.IsNullOrWhitespace(username) || string.IsNullOrWhitespace(email))
            throw new ArgumentException("Username and email are required");
    }
}

public class UserRepository
{
    public void Save(User user) 
    { /* Database operations*/ }
}

public class EmailService
{
    public void SendWelcomeEmail(string email)
    { /* Email sending logic */ }
}

public class UserManager(
    UserValidator validator,
    UserRepository repository,
    EmailService emailService)
{
    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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Python

# Separate responsibilities into different classes
class UserValidator:
    def validate(self, username: str, email: str) -> None:
        if not username or not email:
            raise ValueError("Username and email are required")

class UserRepository:
    def save(self, user: dict) -> None:
        # Database operations
        pass

class EmailService:
    def send_welcome_email(self, email: str) -> None:
        # Email sending logic
        pass

class UserManager:
    def __init__(self, validator: UserValidator, repository: UserRepository, email_service: EmailService):
        self._validator = validator
        self._repository = repository
        self._email_service = email_service

    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)
Enter fullscreen mode Exit fullscreen mode

TypeScript

// Separate responsibilities into different classes
class UserValidator {
    validate(username: string, email: string): void {
        if (!username || !email) {
            throw new Error("Username and email are required");
        }
    }
}

class UserRepository {
    save(user: User): void {
        // Database operations
    }
}

class EmailService {
    sendWelcomeEmail(email: string): void {
        // Email sending logic
    }
}

class UserManager {
    constructor(
        private validator: UserValidator,
        private repository: UserRepository,
        private emailService: EmailService
    ) {}

    createUser(username: string, email: string): void {
        this.validator.validate(username, email);
        const user = { username, email };
        this.repository.save(user);
        this.emailService.sendWelcomeEmail(email);
    }
}
Enter fullscreen mode Exit fullscreen mode

O - Open/Closed Principle (OCP)

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

You should be able to add new functionality without modifying existing code. Use abstractions (interfaces, abstract classes) to achieve this.

❌ Violation Example

C#

public class DiscountCalculator
{
    public decimal CalculateDiscount(string customerType, decimal amount)
    {
        return customerType switch
        {
            "Regular" => amount * 0.1m, // 10% discount
            "Premium" => amount * 0.2m, // 20% discount
            "VIP" => amount * 0.3m, // 30% discount
            _ => 0 // Adding a new customer type requires modifying this method
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Python

class DiscountCalculator:
    def calculate_discount(self, customer_type: str, amount: float) -> float:
        if customer_type == "Regular":
            return amount * 0.1  # 10% discount
        elif customer_type == "Premium":
            return amount * 0.2  # 20% discount
        elif customer_type == "VIP":
            return amount * 0.3  # 30% discount
        # Adding a new customer type requires modifying this method
        return 0
Enter fullscreen mode Exit fullscreen mode

TypeScript

class DiscountCalculator {
    calculateDiscount(customerType: string, amount: number): number {
        if (customerType === "Regular") {
            return amount * 0.1; // 10% discount
        } else if (customerType === "Premium") {
            return amount * 0.2; // 20% discount
        } else if (customerType === "VIP") {
            return amount * 0.3; // 30% discount
        }
        // Adding a new customer type requires modifying this method
        return 0;
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ Correct Implementation

C#

public interface IDiscountStrategy
{
    decimal CalculateDiscount(decimal amount);
}

public class RegularCustomerDiscount : IDiscountStrategy
{
    public decimal CalculateDiscount(decimal amount) => amount * 0.1m;
}

public class PremiumCustomerDiscount : IDiscountStrategy
{
    public decimal CalculateDiscount(decimal amount) => amount * 0.2m;
}

public class VipCustomerDiscount : IDiscountStrategy
{
    public decimal CalculateDiscount(decimal amount) => amount * 0.3m;
}

public class DiscountCalculator(IDiscountStrategy strategy)
{
    public decimal CalculateDiscount(decimal amount) => strategy.CalculateDiscount(amount);
}

// Now you can add new discount types without modifying existing code:
public class GoldCustomerDiscount : IDiscountStrategy
{
    public decimal CalculateDiscount(decimal amount) => amount * 0.25m;
}
Enter fullscreen mode Exit fullscreen mode

Python

from abc import ABC, abstractmethod

class DiscountStrategy(ABC):
    @abstractmethod
    def calculate_discount(self, amount: float) -> float:
        pass

class RegularCustomerDiscount(DiscountStrategy):
    def calculate_discount(self, amount: float) -> float:
        return amount * 0.1

class PremiumCustomerDiscount(DiscountStrategy):
    def calculate_discount(self, amount: float) -> float:
        return amount * 0.2

class VipCustomerDiscount(DiscountStrategy):
    def calculate_discount(self, amount: float) -> float:
        return amount * 0.3

class DiscountCalculator:
    def __init__(self, strategy: DiscountStrategy):
        self._strategy = strategy

    def calculate_discount(self, amount: float) -> float:
        return self._strategy.calculate_discount(amount)

# Now you can add new discount types without modifying existing code:
class GoldCustomerDiscount(DiscountStrategy):
    def calculate_discount(self, amount: float) -> float:
        return amount * 0.25
Enter fullscreen mode Exit fullscreen mode

TypeScript

interface IDiscountStrategy {
    calculateDiscount(amount: number): number;
}

class RegularCustomerDiscount implements IDiscountStrategy {
    calculateDiscount(amount: number): number {
        return amount * 0.1;
    }
}

class PremiumCustomerDiscount implements IDiscountStrategy {
    calculateDiscount(amount: number): number {
        return amount * 0.2;
    }
}

class VipCustomerDiscount implements IDiscountStrategy {
    calculateDiscount(amount: number): number {
        return amount * 0.3;
    }
}

class DiscountCalculator {
    constructor(private strategy: IDiscountStrategy) {}

    calculateDiscount(amount: number): number {
        return this.strategy.calculateDiscount(amount);
    }
}

// Now you can add new discount types without modifying existing code:
class GoldCustomerDiscount implements IDiscountStrategy {
    calculateDiscount(amount: number): number {
        return amount * 0.25;
    }
}
Enter fullscreen mode Exit fullscreen mode

L - Liskov Substitution Principle (LSP)

"Objects of a superclass should be replaceable with objects of its subclasses without breaking the application."

Derived classes should be substitutable for their base classes without altering the correctness of the program.

❌ Violation Example

C#

public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }

    public int GetArea() => Width * Height;
}

public class Square : Rectangle
{
    public override int Width
    {
        get => base.Width;
        set
        {
            base.Width = value;
            base.Height = value; // Violates LSP: Square changes behavior
        }
    }

    public override int Height
    {
        get => base.Height;
        set
        {
            base.Height = value;
            base.Width = value; // Violates LSP
        }
    }
}

// This breaks the substitution principle:
Rectangle rect = new Square();
rect.Width = 5;
rect.Height = 4;
// User expects area = 20, but gets 16 because Square forces width = height
Enter fullscreen mode Exit fullscreen mode

Python

class Rectangle:
    def __init__(self, width: int, height: int):
        self.width = width
        self.height = height

    def get_area(self) -> int:
        return self.width * self.height

class Square(Rectangle):
    def __init__(self, side: int):
        super().__init__(side, side)

    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, value: int):
        self._width = value
        self._height = value  # Violates LSP: Square changes behavior

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value: int):
        self._height = value
        self._width = value  # Violates LSP

# This breaks the substitution principle:
rect: Rectangle = Square(5)
rect.width = 5
rect.height = 4
# User expects area = 20, but gets 16 because Square forces width = height
Enter fullscreen mode Exit fullscreen mode

TypeScript

class Rectangle {
    protected _width: number = 0;
    protected _height: number = 0;

    setWidth(width: number): void {
        this._width = width;
    }

    setHeight(height: number): void {
        this._height = height;
    }

    getArea(): number {
        return this._width * this._height;
    }
}

class Square extends Rectangle {
    setWidth(width: number): void {
        this._width = width;
        this._height = width; // Violates LSP: Square changes behavior
    }

    setHeight(height: number): void {
        this._height = height;
        this._width = height; // Violates LSP
    }
}

// This breaks the substitution principle:
const rect: Rectangle = new Square();
rect.setWidth(5);
rect.setHeight(4);
// User expects area = 20, but gets 16 because Square forces width = height
Enter fullscreen mode Exit fullscreen mode

✅ Correct Implementation

C#

public abstract class Shape
{
    public abstract int GetArea();
}

public class Rectangle : Shape
{
    public int Width { get; set; }
    public int Height { get; set; }

    public override int GetArea() => Width * Height;
}

public class Square : Shape
{
    public int Side { get; set; }

    public override int GetArea() => Side * Side;
}

// Now both can be used interchangeably where Shape is expected:
public class AreaCalculator
{
    public int CalculateTotalArea(List<Shape> shapes) => shapes.Sum(s => s.GetArea());
}
Enter fullscreen mode Exit fullscreen mode

Python

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def get_area(self) -> int:
        pass

class Rectangle(Shape):
    def __init__(self, width: int, height: int):
        self.width = width
        self.height = height

    def get_area(self) -> int:
        return self.width * self.height

class Square(Shape):
    def __init__(self, side: int):
        self.side = side

    def get_area(self) -> int:
        return self.side * self.side

# Now both can be used interchangeably where Shape is expected:
class AreaCalculator:
    def calculate_total_area(self, shapes: list[Shape]) -> int:
        return sum(shape.get_area() for shape in shapes)
Enter fullscreen mode Exit fullscreen mode

TypeScript

abstract class Shape {
    abstract getArea(): number;
}

class Rectangle extends Shape {
    constructor(private width: number, private height: number) {
        super();
    }

    getArea(): number {
        return this.width * this.height;
    }
}

class Square extends Shape {
    constructor(private side: number) {
        super();
    }

    getArea(): number {
        return this.side * this.side;
    }
}

// Now both can be used interchangeably where Shape is expected:
class AreaCalculator {
    calculateTotalArea(shapes: Shape[]): number {
        return shapes.reduce((total, shape) => total + shape.getArea(), 0);
    }
}
Enter fullscreen mode Exit fullscreen mode

I - Interface Segregation Principle (ISP)

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

Instead of one large interface, create smaller, more specific interfaces. This prevents classes from implementing methods they don't need.

❌ Violation Example

C#

public interface IBird
{
    void Fly();
    void Swim();
    void Walk();
}

public class Eagle : IBird
{
    public void Fly() { /* Flying high */ }
    public void Swim() { throw new NotImplementedException(); } // Eagle doesn't swim!
    public void Walk() { /* Walking */ }
}

public class Penguin : IBird
{
    public void Fly() { throw new NotImplementedException(); } // Penguin can't fly!
    public void Swim() { /* Swimming underwater */ }
    public void Walk() { /* Waddling */ }
}

public class Ostrich : IBird
{
    public void Fly() { throw new NotImplementedException(); } // Ostrich can't fly!
    public void Swim() { throw new NotImplementedException(); } // Ostrich doesn't swim!
    public void Walk() { /* Running fast */ }
}
Enter fullscreen mode Exit fullscreen mode

Python

from abc import ABC, abstractmethod

class IBird(ABC):
    @abstractmethod
    def fly(self) -> None:
        pass

    @abstractmethod
    def swim(self) -> None:
        pass

    @abstractmethod
    def walk(self) -> None:
        pass

class Eagle(IBird):
    def fly(self) -> None:
        # Flying high
        pass

    def swim(self) -> None:
        raise NotImplementedError("Eagle doesn't swim!")  # Eagle doesn't swim!

    def walk(self) -> None:
        # Walking
        pass

class Penguin(IBird):
    def fly(self) -> None:
        raise NotImplementedError("Penguin can't fly!")  # Penguin can't fly!

    def swim(self) -> None:
        # Swimming underwater
        pass

    def walk(self) -> None:
        # Waddling
        pass

class Ostrich(IBird):
    def fly(self) -> None:
        raise NotImplementedError("Ostrich can't fly!")  # Ostrich can't fly!

    def swim(self) -> None:
        raise NotImplementedError("Ostrich doesn't swim!")  # Ostrich doesn't swim!

    def walk(self) -> None:
        # Running fast
        pass
Enter fullscreen mode Exit fullscreen mode

TypeScript

interface IBird {
    fly(): void;
    swim(): void;
    walk(): void;
}

class Eagle implements IBird {
    fly(): void {
        // Flying high
    }

    swim(): void {
        throw new Error("Eagle doesn't swim!"); // Eagle doesn't swim!
    }

    walk(): void {
        // Walking
    }
}

class Penguin implements IBird {
    fly(): void {
        throw new Error("Penguin can't fly!"); // Penguin can't fly!
    }

    swim(): void {
        // Swimming underwater
    }

    walk(): void {
        // Waddling
    }
}

class Ostrich implements IBird {
    fly(): void {
        throw new Error("Ostrich can't fly!"); // Ostrich can't fly!
    }

    swim(): void {
        throw new Error("Ostrich doesn't swim!"); // Ostrich doesn't swim!
    }

    walk(): void {
        // Running fast
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ Correct Implementation

C#

public interface IFlyable
{
    void Fly();
}

public interface ISwimmable
{
    void Swim();
}

public interface IWalkable
{
    void Walk();
}

public class Eagle : IFlyable, IWalkable
{
    public void Fly() { /* Flying high */ }
    public void Walk() { /* Walking */ }
    // No need to implement Swim()
}

public class Penguin : ISwimmable, IWalkable
{
    public void Swim() { /* Swimming underwater */ }
    public void Walk() { /* Waddling */ }
    // No need to implement Fly()
}

public class Ostrich : IWalkable
{
    public void Walk() { /* Running fast */ }
    // No need to implement Fly() or Swim()
}

public class Duck : IFlyable, ISwimmable, IWalkable
{
    public void Fly() { /* Flying */ }
    public void Swim() { /* Swimming */ }
    public void Walk() { /* Walking */ }
}
Enter fullscreen mode Exit fullscreen mode

Python

from abc import ABC, abstractmethod

class IFlyable(ABC):
    @abstractmethod
    def fly(self) -> None:
        pass

class ISwimmable(ABC):
    @abstractmethod
    def swim(self) -> None:
        pass

class IWalkable(ABC):
    @abstractmethod
    def walk(self) -> None:
        pass

class Eagle(IFlyable, IWalkable):
    def fly(self) -> None:
        # Flying high
        pass

    def walk(self) -> None:
        # Walking
        pass
    # No need to implement swim()

class Penguin(ISwimmable, IWalkable):
    def swim(self) -> None:
        # Swimming underwater
        pass

    def walk(self) -> None:
        # Waddling
        pass
    # No need to implement fly()

class Ostrich(IWalkable):
    def walk(self) -> None:
        # Running fast
        pass
    # No need to implement fly() or swim()

class Duck(IFlyable, ISwimmable, IWalkable):
    def fly(self) -> None:
        # Flying
        pass

    def swim(self) -> None:
        # Swimming
        pass

    def walk(self) -> None:
        # Walking
        pass
Enter fullscreen mode Exit fullscreen mode

TypeScript

interface IFlyable {
    fly(): void;
}

interface ISwimmable {
    swim(): void;
}

interface IWalkable {
    walk(): void;
}

class Eagle implements IFlyable, IWalkable {
    fly(): void {
        // Flying high
    }

    walk(): void {
        // Walking
    }
    // No need to implement swim()
}

class Penguin implements ISwimmable, IWalkable {
    swim(): void {
        // Swimming underwater
    }

    walk(): void {
        // Waddling
    }
    // No need to implement fly()
}

class Ostrich implements IWalkable {
    walk(): void {
        // Running fast
    }
    // No need to implement fly() or swim()
}

class Duck implements IFlyable, ISwimmable, IWalkable {
    fly(): void {
        // Flying
    }

    swim(): void {
        // Swimming
    }

    walk(): void {
        // Walking
    }
}
Enter fullscreen mode Exit fullscreen mode

D - Dependency Inversion Principle (DIP)

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

Depend on abstractions (interfaces) rather than concrete implementations. This makes your code more flexible and testable.

❌ Violation Example

C#

// High-level module depends on low-level module
public class OrderService
{
    private readonly SqlServerDatabase _database; // Direct dependency

    public OrderService()
    {
        _database = new SqlServerDatabase(); // Tightly coupled
    }

    public void SaveOrder(Order order)
    {
        _database.Save(order);
    }
}

public class SqlServerDatabase
{
    public void Save(object data) { /* SQL Server specific code */ }
}
Enter fullscreen mode Exit fullscreen mode

Python

# High-level module depends on low-level module
class OrderService:
    def __init__(self):
        self._database = PostgreSQLDatabase()  # Direct dependency, tightly coupled

    def save_order(self, order: dict) -> None:
        self._database.save(order)

class PostgreSQLDatabase:
    def save(self, data: dict) -> None:
        # PostgreSQL specific code
        pass
Enter fullscreen mode Exit fullscreen mode

TypeScript

// High-level module depends on low-level module
class OrderService {
    private database: MySQLDatabase; // Direct dependency

    constructor() {
        this.database = new MySQLDatabase(); // Tightly coupled
    }

    saveOrder(order: Order): void {
        this.database.save(order);
    }
}

class MySQLDatabase {
    save(data: any): void {
        // MySQL specific code
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ Correct Implementation

C#

// Both depend on abstraction
public interface IRepository
{
    void Save<T>(T entity);
}

public class OrderService
{
    private readonly IRepository _repository;

    public OrderService(IRepository repository) // Dependency injection
    {
        _repository = repository;
    }

    public void SaveOrder(Order order) => _repository.Save(order);
}

// Low-level modules implement the abstraction
public class SqlServerRepository : IRepository
{
    public void Save<T>(T entity) { /* SQL Server specific code */ }
}

public class PostgreSQLRepository : IRepository
{
    public void Save<T>(T entity) { /* PostgreSQL specific code */ }
}

// Usage with dependency injection:
var orderService = new OrderService(new SqlServerRepository());
// Or easily switch to PostgreSQL:
// var orderService = new OrderService(new PostgreSQLRepository());
Enter fullscreen mode Exit fullscreen mode

Python

from abc import ABC, abstractmethod

# Both depend on abstraction
class IRepository(ABC):
    @abstractmethod
    def save(self, entity: dict) -> None:
        pass

class OrderService:
    def __init__(self, repository: IRepository):  # Dependency injection
        self._repository = repository

    def save_order(self, order: dict) -> None:
        self._repository.save(order)

# Low-level modules implement the abstraction
class PostgreSQLRepository(IRepository):
    def save(self, entity: dict) -> None:
        # PostgreSQL specific code
        pass

class MongoDBRepository(IRepository):
    def save(self, entity: dict) -> None:
        # MongoDB specific code
        pass

# Usage with dependency injection:
order_service = OrderService(PostgreSQLRepository())
# Or easily switch to MongoDB:
# order_service = OrderService(MongoDBRepository())
Enter fullscreen mode Exit fullscreen mode

TypeScript

// Both depend on abstraction
interface IRepository {
    save<T>(entity: T): void;
}

class OrderService {
    constructor(private repository: IRepository) {} // Dependency injection

    saveOrder(order: Order): void {
        this.repository.save(order);
    }
}

// Low-level modules implement the abstraction
class MySQLRepository implements IRepository {
    save<T>(entity: T): void {
        // MySQL specific code
    }
}

class MongoDBRepository implements IRepository {
    save<T>(entity: T): void {
        // MongoDB specific code
    }
}

// Usage with dependency injection:
const orderService = new OrderService(new MySQLRepository());
// Or easily switch to MongoDB:
// const orderService = new OrderService(new MongoDBRepository());
Enter fullscreen mode Exit fullscreen mode

Benefits of Following SOLID Principles

When you apply SOLID principles consistently, you'll notice:

  1. Easier Maintenance: Changes are localized and don't break unrelated features
  2. Better Testability: Smaller, focused classes with dependency injection are much easier to unit test
  3. Improved Flexibility: Easy to swap implementations and add new features without modifying existing code
  4. Reduced Coupling: Classes depend on abstractions, not concrete implementations
  5. Better Code Reusability: Components can be reused in different contexts

How SOLID Improves Testability

SOLID principles make unit testing significantly easier. Here's a practical comparison:

❌ Without SOLID - Hard to Test

public class OrderService
{
    public void ProcessOrder(Order order)
    {
        // Direct database dependency - requires real database for testing
        using (var connection = new SqlConnection("connection string"))
        {
            connection.Open();
            // ... save order
        }

        // Direct email dependency - sends real emails during tests
        var smtp = new SmtpClient();
        smtp.Send("customer@email.com", "Order confirmed", "...");
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ With SOLID - Easy to Test

public class OrderService
{
    private readonly IRepository _repository;
    private readonly IEmailService _emailService;

    public OrderService(IRepository repository, IEmailService emailService)
    {
        _repository = repository;
        _emailService = emailService;
    }

    public void ProcessOrder(Order order)
    {
        _repository.Save(order);
        _emailService.SendConfirmation(order.CustomerEmail);
    }
}

// Now you can test with mocks - fast, isolated, no external dependencies
[Test]
public void ProcessOrder_SavesOrderAndSendsEmail()
{
    var mockRepo = new Mock<IRepository>();
    var mockEmail = new Mock<IEmailService>();
    var service = new OrderService(mockRepo.Object, mockEmail.Object);

    service.ProcessOrder(new Order());

    mockRepo.Verify(r => r.Save(It.IsAny<Order>()), Times.Once);
    mockEmail.Verify(e => e.SendConfirmation(It.IsAny<string>()), Times.Once);
}
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • ✅ Tests run in milliseconds (no database/network calls)
  • ✅ Tests are isolated (no side effects)
  • ✅ Easy to test edge cases and error scenarios
  • ✅ No test environment setup required

Performance Considerations

While SOLID principles improve code quality, they can have minor performance implications:

Potential Trade-offs:

  • Indirection Overhead: Interface calls have a tiny overhead compared to direct calls (negligible in most applications)
  • Memory: More objects mean slightly more memory usage (usually insignificant)
  • Initialization: Dependency injection containers add minimal startup time

When Performance Matters:

  • High-frequency code paths (called millions of times per second): Consider direct dependencies
  • Real-time systems: May need to balance SOLID with performance requirements
  • Memory-constrained environments: Evaluate if the abstraction overhead is acceptable

Best Practice: Apply SOLID principles first, then optimize only if profiling shows actual performance issues. The maintainability benefits usually far outweigh the minimal performance cost.

Conclusion

SOLID principles are foundational to writing clean, maintainable code. They help us build software that can evolve and adapt to changing requirements. Remember: these are guidelines, not strict rules. Start simple, refactor when you see concrete problems, and find the right balance for your context.

Start applying these principles in your next project, and you'll see the difference in code quality and maintainability!


💡 Want to practice hands-on? You can find this exercise with starter code in multiple languages in the GitHub repository. Choose your preferred language (ava, Ruby, C#, Python, or TypeScript) and refactor the code following all SOLID principles!


Quick Reference

Principle One-Liner When to Apply Key Benefit
Single Responsibility One class, one job When a class handles multiple concerns Easier maintenance and testing
Open/Closed Extend, don't modify When adding features requires changing existing code Reduced risk of breaking changes
Liskov Substitution Subclasses must be substitutable When using inheritance Predictable behavior
Interface Segregation Many specific interfaces, not one large one When classes implement methods they don't need Reduced coupling
Dependency Inversion Depend on abstractions, not concretions When classes depend on concrete implementations Better testability and flexibility

Happy coding! 🚀

Top comments (0)