Skip to content

Overview

The SOLID principles are five design principles that help developers create software that is maintainable, scalable, and easy to understand. These principles are particularly relevant in object-oriented programming (OOP) and are fundamental in crafting clean architectures.

Single Responsibility Principle (SRP)

A class should have only one reason to change.

Explanation

Each class should have one job and should only change for one reason. If a class has multiple responsibilities, modifying one aspect of it can inadvertently impact unrelated functionalities.

Example:

    // Violates SRP: This class does too much – handling user data and logging
    public class UserManager
    {
        public void AddUser(string username)
        {
            Console.WriteLine("User added: " + username);
            LogToFile("User added: " + username);
        }

        private void LogToFile(string message)
        {
            File.WriteAllText("log.txt", message);
        }
    }
    // Following SRP: Separate concerns into dedicated classes
    public class UserManager
    {
        private readonly ILogger _logger;
        public UserManager(ILogger logger) => _logger = logger;

        public void AddUser(string username)
        {
            Console.WriteLine("User added: " + username);
            _logger.Log("User added: " + username);
        }
    }

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

    public class FileLogger : ILogger
    {
        public void Log(string message) => File.WriteAllText("log.txt", message);
    }

Tips & Pitfalls

Tip: Break down responsibilities early—don’t let classes grow into "God classes."
⚠️ Pitfall: Taking SRP too far can result in an explosion of micro-classes, making the system overly complex.

Open/Closed Principle (OCP)

A class should be open for extension, but closed for modification.

Explanation

The idea is that you should be able to extend functionality without modifying existing code. This is useful when adding new features without breaking existing ones.

Example:

    // Violates OCP: Each new discount type requires modifying the method
    public class DiscountService
    {
        public decimal ApplyDiscount(decimal price, string discountType)
        {
            if (discountType == "Student")
                return price * 0.9m;
            else if (discountType == "Senior")
                return price * 0.8m;
            return price;
        }
    }
    // Following OCP: Use polymorphism to allow extension without modification
    public interface IDiscount
    {
        decimal Apply(decimal price);
    }

    public class StudentDiscount : IDiscount
    {
        public decimal Apply(decimal price) => price * 0.9m;
    }

    public class SeniorDiscount : IDiscount
    {
        public decimal Apply(decimal price) => price * 0.8m;
    }

    public class DiscountService
    {
        public decimal ApplyDiscount(decimal price, IDiscount discount) => discount.Apply(price);
    }

Tips & Pitfalls

Tip: Use abstraction (interfaces, base classes) to allow extension.
⚠️ Pitfall: Overuse of inheritance can lead to rigid structures—favor composition over inheritance when possible.

Liskov Substitution Principle (LSP)

Subtypes must be substitutable for their base types without altering correctness.

Explanation

A subclass should be a true subtype of its base class and should not break expected behavior.

Example:

    // Violates LSP: A rectangle expects width and height to be independent
    public class Rectangle
    {
        public virtual int Width { get; set; }
        public virtual int Height { get; set; }
        public int Area => Width * Height;
    }

    public class Square : Rectangle
    {
        public override int Width
        {
            set { base.Width = base.Height = value; }
        }
        public override int Height
        {
            set { base.Width = base.Height = value; }
        }
    }

Here, if we substitute a Square where a Rectangle is expected, we break the behaviour, leading to unexpected results.

    // Correct Approach: Avoid inheritance when it doesn’t model the real-world relationship.
    public interface IShape
    {
        int Area { get; }
    }

    public class Rectangle : IShape
    {
        public int Width { get; set; }
        public int Height { get; set; }
        public int Area => Width * Height;
    }

    public class Square : IShape
    {
        public int SideLength { get; set; }
        public int Area => SideLength * SideLength;
    }

Tips & Pitfalls

Tip: Favor composition over inheritance if behaviors differ.
⚠️ Pitfall: If a subclass removes functionality from a base class, it likely violates LSP.

Interface Segregation Principle (ISP)

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

Explanation

Large, bloated interfaces force classes to implement methods they don’t need. Instead, split interfaces into smaller, more specific ones.

Example:

    // Violates ISP: The interface forces every implementation to define all methods
    public interface IWorker
    {
        void Work();
        void Eat();
    }

    public class RobotWorker : IWorker
    {
        public void Work() => Console.WriteLine("Working...");
        public void Eat() => throw new NotImplementedException(); // Not applicable!
    }
    // Following ISP: Split interfaces based on responsibility
    public interface IWorkable
    {
        void Work();
    }

    public interface IEatable
    {
        void Eat();
    }

    public class HumanWorker : IWorkable, IEatable
    {
        public void Work() => Console.WriteLine("Working...");
        public void Eat() => Console.WriteLine("Eating...");
    }

    public class RobotWorker : IWorkable
    {
        public void Work() => Console.WriteLine("Working...");
    }

Tips & Pitfalls

Tip: Design interfaces around behaviors rather than trying to create a one-size-fits-all interface.
⚠️ Pitfall: Too many interfaces can lead to excessive fragmentation, making the codebase harder to navigate.

Dependency Inversion Principle (DIP)

Depend on abstractions, not on concrete implementations.

Explanation

High-level modules should not depend on low-level modules; both should depend on abstractions. This improves maintainability and testability.

Example:

    // Violates DIP: Direct dependency on concrete class
    public class EmailService
    {
        public void SendEmail(string message) => Console.WriteLine("Email sent: " + message);
    }

    public class NotificationService
    {
        private EmailService _emailService = new EmailService();

        public void Notify(string message)
        {
            _emailService.SendEmail(message);
        }
    }
    // Following DIP: Depend on abstraction (interface)
    public interface INotificationChannel
    {
        void Send(string message);
    }

    public class EmailService : INotificationChannel
    {
        public void Send(string message) => Console.WriteLine("Email sent: " + message);
    }

    public class NotificationService
    {
        private readonly INotificationChannel _channel;

        public NotificationService(INotificationChannel channel)
        {
            _channel = channel;
        }

        public void Notify(string message)
        {
            _channel.Send(message);
        }
    }

Tips & Pitfalls

Tip: Use dependency injection (DI) frameworks to manage dependencies efficiently.
⚠️ Pitfall: Over-abstraction can make code harder to read if applied unnecessarily.

Final Thoughts

  • SOLID principles improve software scalability, maintainability, and testability.

  • Avoid overengineering—apply these principles where they add value.

  • They work best in combination rather than in isolation.