WHAT ARE SOLID PRINCIPLES?
Introduction
SOLID is a mnemonic acronym for five design principles that help you create maintainable, scalable, and flexible
code.
These principles, introduced by Robert C. Martin (Uncle Bob), are essential guidelines for writing clean code in
object-oriented programming.
Why SOLID Matters?
Let's say you created an application that works perfectly today. But after 6 months, you need to add new features.
When you try to modify the code:
Problems Start Appearing:
Small change breaks something else
Adding features becomes slower
Understanding the code becomes harder
Fixing bugs introduces new bugs
Testing is complicated
That is when developers face the struggle of technical debt.
We need principles to write code that's easy to change, easy to test, and easy to understand.
THE FIVE PRINCIPLES
S - SINGLE RESPONSIBILITY PRINCIPLE (SRP)
"A class should have only one reason to change."
This means each class should have only one job or responsibility.
The Problem:
// ❌ VIOLATES SRP - Multiple Responsibilities
public class User
{
public string Username { get; set; }
public string Email { get; set; }
// Responsibility 1: User Management
public void Register()
{
// Save to database
[Link]("Saving user to database...");
}
// Responsibility 2: Email Sending
public void SendWelcomeEmail()
{
// Send email logic
[Link]("Sending welcome email...");
}
// Responsibility 3: Password Hashing
public string HashPassword(string password)
{
// Hashing logic
return "hashed_" + password;
}
}
Problems:
If registration logic changes, we modify User class
If email template changes, we modify User class
If hashing algorithm changes, we modify User class
3 reasons to change = 3 responsibilities!
The Solution:
// ✅ FOLLOWS SRP - Single Responsibility Each
public class User
{
public string Username { get; set; }
public string Email { get; set; }
// Only responsible for User data
public void Register()
{
[Link]("Registering user...");
}
}
public class EmailService
{
// Only responsible for sending emails
public void SendWelcomeEmail(User user)
{
[Link]($"Sending welcome email to {[Link]}");
}
}
public class PasswordService
{
// Only responsible for password hashing
public string HashPassword(string password)
{
return "hashed_" + password;
}
}
Benefits:
✨ Easier Testing - Test each responsibility independently
✨ Better Maintainability - Change one thing without affecting others
✨ Clearer Code - Each class has obvious purpose
✨ Reusability - Easier to reuse in different contexts
O - OPEN/CLOSED PRINCIPLE (OCP)
"Software should be open for extension, closed for modification."
This means you should add new features without modifying existing code.
The Problem:
// ❌ VIOLATES OCP - Must modify existing code for new types
public class NotificationService
{
public void SendNotification(string type, string message)
{
if (type == "Email")
{
SendEmail(message);
}
else if (type == "SMS")
{
SendSMS(message);
}
else if (type == "Push")
{
SendPushNotification(message);
}
// Add new notification type? MODIFY THIS CLASS!
}
private void SendEmail(string message) =>
[Link]($"Email: {message}");
private void SendSMS(string message) =>
[Link]($"SMS: {message}");
private void SendPushNotification(string message) =>
[Link]($"Push: {message}");
}
Problems:
Adding Slack notification requires modifying NotificationService
Adding WhatsApp requires modifying NotificationService
Every new type means changing existing code
Risk of breaking existing functionality
The Solution:
// ✅ FOLLOWS OCP - Extend without modifying
public interface INotificationChannel
{
void Send(string message);
}
public class EmailNotification : INotificationChannel
{
public void Send(string message) =>
[Link]($"Email: {message}");
}
public class SMSNotification : INotificationChannel
{
public void Send(string message) =>
[Link]($"SMS: {message}");
}
public class PushNotification : INotificationChannel
{
public void Send(string message) =>
[Link]($"Push: {message}");
}
// Add new notification WITHOUT modifying existing code
public class SlackNotification : INotificationChannel
{
public void Send(string message) =>
[Link]($"Slack: {message}");
}
public class NotificationService
{
private readonly INotificationChannel _channel;
public NotificationService(INotificationChannel channel)
{
_channel = channel;
}
public void SendNotification(string message)
{
_channel.Send(message); // Uses any notification type
}
}
// Usage
var emailNotification = new EmailNotification();
var service = new NotificationService(emailNotification);
[Link]("Hello!");
Benefits:
✨ Safe to Extend - Add features without breaking existing code
✨ Reduced Risk - Existing functionality never modified
✨ Easier Testing - Test new implementations independently
✨ Better Design - Forces use of abstractions
L - LISKOV SUBSTITUTION PRINCIPLE (LSP)
"Subtypes must be substitutable for their base types."
This means a derived class should work correctly wherever its base class is used.
The Problem:
// ❌ VIOLATES LSP - Square breaks Rectangle contract
public class Rectangle
{
public virtual double Width { get; set; }
public virtual double Height { get; set; }
public virtual double CalculateArea()
{
return Width * Height;
}
}
public class Square : Rectangle
{
// Square: all sides must be equal
private double _side;
public override double Width
{
get => _side;
set => _side = value; // Sets both width and height
}
public override double Height
{
get => _side;
set => _side = value; // Sets both width and height
}
}
// Usage - PROBLEM!
Rectangle shape = new Square { Width = 5, Height = 10 };
// We set Width=5 and Height=10, but Square makes them both 10
double area = [Link](); // 100, not 50!
// Expected 50, got 100 - BROKEN CONTRACT!
Problems:
Square doesn't behave like Rectangle
Substituting Square for Rectangle breaks code
Violates the Liskov contract
The Solution:
// ✅ FOLLOWS LSP - Proper interface design
public interface IShape
{
double CalculateArea();
}
public class Rectangle : IShape
{
public double Width { get; set; }
public double Height { get; set; }
public double CalculateArea()
{
return Width * Height;
}
}
public class Square : IShape
{
public double Side { get; set; }
public double CalculateArea()
{
return Side * Side;
}
}
// Usage - All shapes work correctly
IShape rectangle = new Rectangle { Width = 5, Height = 10 };
IShape square = new Square { Side = 5 };
[Link]([Link]()); // 50 ✓
[Link]([Link]()); // 25 ✓
// Can substitute any IShape without breaking
void PrintArea(IShape shape)
{
[Link]($"Area: {[Link]()}");
}
PrintArea(rectangle); // Works!
PrintArea(square); // Works!
Benefits:
✨ Reliable Substitution - Derived classes behave predictably
✨ Contract Honored - Base class expectations met
✨ Polymorphism Works - Can use base type safely
✨ Predictable Code - No surprises when substituting
I - INTERFACE SEGREGATION PRINCIPLE (ISP)
"Clients should not depend on interfaces they don't use."
This means create specific interfaces rather than one large interface.
The Problem:
// ❌ VIOLATES ISP - Forces unnecessary implementation
public interface IWorker
{
void Work();
void Eat();
void Sleep();
}
public class HumanWorker : IWorker
{
public void Work() => [Link]("Human working...");
public void Eat() => [Link]("Human eating...");
public void Sleep() => [Link]("Human sleeping...");
}
public class RobotWorker : IWorker
{
public void Work() => [Link]("Robot working...");
// Robot doesn't eat or sleep! But forced to implement them
public void Eat() => throw new NotImplementedException();
public void Sleep() => throw new NotImplementedException();
}
Problems:
RobotWorker forced to implement unnecessary methods
Methods throw NotImplementedException
Interface too broad for all clients
The Solution:
// ✅ FOLLOWS ISP - Segregated, specific interfaces
public interface IWorker
{
void Work();
}
public interface ILivingBeing
{
void Eat();
void Sleep();
}
public class HumanWorker : IWorker, ILivingBeing
{
public void Work() => [Link]("Human working...");
public void Eat() => [Link]("Human eating...");
public void Sleep() => [Link]("Human sleeping...");
}
public class RobotWorker : IWorker
{
public void Work() => [Link]("Robot working...");
// No unnecessary Eat() or Sleep() methods!
}
// Usage
IWorker humanWorker = new HumanWorker();
IWorker robotWorker = new RobotWorker();
[Link](); // ✓ Works
[Link](); // ✓ Works
// For living beings
ILivingBeing human = new HumanWorker();
[Link](); // ✓ Works
[Link](); // ✓ Works
Benefits:
✨ Focused Interfaces - Only what's needed
✨ No Empty Implementations - No dummy methods
✨ Flexible Design - Classes implement only relevant interfaces
✨ Better Clarity - Clear contract for each interface
D - DEPENDENCY INVERSION PRINCIPLE (DIP)
"High-level modules should not depend on low-level modules. Both should depend on abstractions."
This means depend on abstractions (interfaces), not concrete implementations.
The Problem:
// ❌ VIOLATES DIP - Depends on concrete implementations
public class FileLogger
{
public void Log(string message)
{
// Log to file
[Link]($"File Log: {message}");
}
}
public class UserService
{
private FileLogger _logger; // Direct dependency!
public UserService()
{
_logger = new FileLogger(); // Hard-coded dependency
}
public void CreateUser(string name)
{
_logger.Log($"Creating user: {name}");
// Create user logic
}
}
Problems:
UserService tightly coupled to FileLogger
Cannot use DatabaseLogger without changing UserService
Hard to test - must use real FileLogger
Cannot swap logging implementation
The Solution:
// ✅ FOLLOWS DIP - Depends on abstractions
public interface ILogger
{
void Log(string message);
}
public class FileLogger : ILogger
{
public void Log(string message) =>
[Link]($"File Log: {message}");
}
public class DatabaseLogger : ILogger
{
public void Log(string message) =>
[Link]($"DB Log: {message}");
}
public class ConsoleLogger : ILogger
{
public void Log(string message) =>
[Link]($"Console Log: {message}");
}
public class UserService
{
private readonly ILogger _logger; // Depend on abstraction
public UserService(ILogger logger) // Injected dependency
{
_logger = logger;
}
public void CreateUser(string name)
{
_logger.Log($"Creating user: {name}");
// Create user logic
}
}
// Usage - Can swap any logger
ILogger fileLogger = new FileLogger();
ILogger dbLogger = new DatabaseLogger();
ILogger consoleLogger = new ConsoleLogger();
var service1 = new UserService(fileLogger);
var service2 = new UserService(dbLogger);
var service3 = new UserService(consoleLogger);
[Link]("John"); // Uses FileLogger
[Link]("Jane"); // Uses DatabaseLogger
[Link]("Bob"); // Uses ConsoleLogger
Benefits:
✨ Loose Coupling - Classes independent of implementations
✨ Easy to Test - Mock logger for testing
✨ Flexible Implementation - Swap implementations easily
✨ Better Maintainability - Changes isolated to specific class
SOLID PRINCIPLES SUMMARY TABLE
Principle Focus Problem Solution
SRP Single Responsibility Multiple reasons to change One responsibility per class
OCP Open/Closed Must modify existing code Extend without modification
LSP Liskov Substitution Derived class breaks contract Derived class honors base contract
ISP Interface Segregation Force unused implementations Segregated, specific interfaces
DIP Dependency Inversion Depends on concrete classes Depend on abstractions
BENEFITS OF FOLLOWING SOLID
✅ Better Code Maintainability
Clear responsibility for each class
Easy to understand code organization
Simple to locate and modify features
✅ Improved Testability
Unit test components independently
Mock dependencies easily
Test in isolation without database or external services
✅ Enhanced Flexibility
Add features without breaking existing code
Swap implementations easily
Adapt to changing requirements quickly
✅ Reduced Technical Debt
Prevent code smells by design
Avoid tight coupling from the start
Save time on refactoring later
✅ Better Team Collaboration
Clear interfaces between components
Less code conflicts when working together
Easier onboarding for new developers
Well-defined responsibilities for each person
✅ Increased Reusability
Decouple components for reuse
Use in multiple projects easily
Reduce duplicate code
COMMON VIOLATIONS & HOW TO FIX
Violation 1: God Classes
Problem: One class doing too much
// ❌ Bad - God Class
public class UserManager
{
public void Register() { }
public void Login() { }
public void SendEmail() { }
public void ProcessPayment() { }
public void LogActivity() { }
public void ValidateEmail() { }
public void UpdateProfile() { }
}
Fix: Split into focused classes
// ✅ Good - Separated Responsibilities
public class UserRegistration { }
public class UserAuthentication { }
public class EmailService { }
public class PaymentProcessor { }
public class ActivityLogger { }
public class EmailValidator { }
public class UserProfileManager { }
Violation 2: Tight Coupling
Problem: Direct dependencies on concrete classes
// ❌ Bad - Tightly Coupled
public class OrderService
{
private OrderDatabase _db = new OrderDatabase();
private EmailService _email = new EmailService();
public void CreateOrder(Order order)
{
_db.Save(order);
_email.SendConfirmation(order);
}
}
Fix: Use dependency injection with interfaces
// ✅ Good - Loosely Coupled
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly IEmailService _email;
public OrderService(IOrderRepository repository,
IEmailService email)
{
_repository = repository;
_email = email;
}
public void CreateOrder(Order order)
{
_repository.Save(order);
_email.SendConfirmation(order);
}
}
Violation 3: Fat Interfaces
Problem: Interfaces with methods not all clients need
// ❌ Bad - Fat Interface
public interface IDocument
{
void Open();
void Close();
void Save();
void Print();
void Scan();
void Fax();
}
public class TextFile : IDocument
{
public void Open() { }
public void Close() { }
public void Save() { }
public void Print() { }
public void Scan() { throw new NotImplementedException(); } // Unwanted!
public void Fax() { throw new NotImplementedException(); } // Unwanted!
}
Fix: Segregate into specific interfaces
// ✅ Good - Segregated Interfaces
public interface IOpenable
{
void Open();
void Close();
}
public interface ISaveable
{
void Save();
}
public interface IPrintable
{
void Print();
}
public interface IScannable
{
void Scan();
}
public class TextFile : IOpenable, ISaveable, IPrintable
{
public void Open() { }
public void Close() { }
public void Save() { }
public void Print() { }
// Only implements what it needs!
}
Violation 4: Switch/If-Else Chains
Problem: Multiple conditional branches for different types
// ❌ Bad - Switch Statement (Violates OCP)
public decimal CalculateDiscount(Customer customer)
{
switch ([Link])
{
case [Link]:
return 0.05m;
case [Link]:
return 0.15m;
case [Link]:
return 0.25m;
default:
return 0m;
}
// Adding new customer type? Must modify this method!
}
Fix: Use polymorphism
// ✅ Good - Polymorphism (Follows OCP)
public interface ICustomer
{
decimal GetDiscount();
}
public class RegularCustomer : ICustomer
{
public decimal GetDiscount() => 0.05m;
}
public class PremiumCustomer : ICustomer
{
public decimal GetDiscount() => 0.15m;
}
public class VIPCustomer : ICustomer
{
public decimal GetDiscount() => 0.25m;
}
public decimal CalculateDiscount(ICustomer customer)
{
return [Link]();
// Add new customer type? Just create new class!
}
REAL-WORLD ANALOGY
Think of a Restaurant Kitchen:
Without SOLID (Chaos):
One chef does everything: cooking, cleaning, accounting, hiring
Chef must learn all skills
If chef quits, everything stops
Hard to add new dishes
With SOLID (Well-Organized):
SRP: Head Chef → Sauce Chef → Pastry Chef (each has one job)
OCP: Add new dish without changing existing recipes
LSP: Any sauce chef can substitute for another
ISP: Each station has specific tools needed
DIP: Operations depend on job descriptions, not specific people
Result: Efficient, Scalable, Flexible Kitchen!
APPLYING SOLID IN YOUR .NET PROJECTS
Step 1: Identify Classes with Multiple Responsibilities
List what each class does
If more than one reason to change → Split it
Step 2: Create Abstractions
Define interfaces for behavior
Depend on interfaces, not concrete classes
Step 3: Use Dependency Injection
Constructor inject dependencies
Use .NET DI container
Register interfaces with implementations
Step 4: Refactor Gradually
SOLID isn't all-or-nothing
Refactor one principle at a time
Test after each change
Step 5: Code Review with SOLID in Mind
Do classes have single responsibility?
Can we extend without modification?
Can we substitute implementations?
Are interfaces focused?
Do we depend on abstractions?
TOOLS & LIBRARIES
Dependency Injection Containers
[Link] - Built-in .NET
Autofac - Advanced DI features
Ninject - Simple and powerful
StructureMap - Convention-based DI
Code Analysis Tools
SonarAnalyzer - Detect violations
FxCop - Static code analysis
StyleCop - Code style enforcement
R# (ReSharper) - IntelliSense for SOLID
Testing Frameworks
xUnit - Unit testing
NUnit - Microsoft testing
Moq - Mocking framework
FakeItEasy - Mocking and stubbing
KEY TAKEAWAYS
✨ SRP = One responsibility per class
✨ OCP = Open for extension, closed for modification
✨ LSP = Derived classes honor base contracts
✨ ISP = Specific interfaces, not fat interfaces
✨ DIP = Depend on abstractions, not concrete classes
✨ Start Simple = Apply gradually, not all at once
✨ Understand the Why = Not just the what
✨ Use Interfaces = Foundation of all SOLID principles
✨ Test Everything = SOLID code is testable
✨ Refactor Continuously = Improve over time
SO YOU WILL ALWAYS...
Write code that's easy to understand
Make changes safely without breaking things
Add features quickly without fear
Test components independently
Maintain your codebase effortlessly
Scale your application confidently
Welcome new developers with clarity
Reduce bugs through better design
SOLID Principles = Professional Code = Happy Developers!