The SOLID principles are five design principles introduced by Robert C. Martin (Uncle Bob) to make software more maintainable, scalable, and testable.
They are:
- Single Responsibility Principle (SRP)
- Open/Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
Let’s break each one down with bad (violating) and good (fixed) C# examples.
1. Single Responsibility Principle (SRP)
A class should have only one reason to change.
❌ Violation
public class Invoice
{
public void CalculateTotal() { /* business logic */ }
public void PrintInvoice() { /* printing logic */ }
public void SaveToFile(string path) { /* persistence logic */ }
}
Here Invoice is doing three jobs:
- business calculation
- printing
- persistence
If any of these responsibilities changes, Invoice must change → violation.
✅ Fix
public class Invoice
{
public void CalculateTotal() { /* business logic */ }
}
public class InvoicePrinter
{
public void Print(Invoice invoice) { /* printing logic */ }
}
public class InvoicePersistence
{
public void SaveToFile(Invoice invoice, string path) { /* persistence logic */ }
}
Now each class has one reason to change.
2. Open/Closed Principle (OCP)
Classes should be open for extension but closed for modification.
❌ Violation
public class DiscountCalculator
{
public decimal CalculateDiscount(string customerType, decimal amount)
{
if (customerType == "Regular")
return amount * 0.1m;
else if (customerType == "Premium")
return amount * 0.2m;
else
return 0;
}
}
If you add a new customer type, you must modify this method → breaks OCP.
✅ Fix (use polymorphism/specification)
public interface IDiscountRule
{
decimal CalculateDiscount(decimal amount);
}
public class RegularDiscount : IDiscountRule
{
public decimal CalculateDiscount(decimal amount) => amount * 0.1m;
}
public class PremiumDiscount : IDiscountRule
{
public decimal CalculateDiscount(decimal amount) => amount * 0.2m;
}
public class DiscountCalculator
{
private readonly Dictionary<string, IDiscountRule> rules;
public DiscountCalculator()
{
rules = new Dictionary<string, IDiscountRule>
{
{ "Regular", new RegularDiscount() },
{ "Premium", new PremiumDiscount() }
};
}
public decimal CalculateDiscount(string customerType, decimal amount)
{
return rules.ContainsKey(customerType) ? rules[customerType].CalculateDiscount(amount) : 0;
}
}
Now if a new type (e.g., VIP) is added, you add a new class, not modify existing code.
3. Liskov Substitution Principle (LSP)
Subtypes must be replaceable for their base type without breaking behavior.
❌ Violation
public class Bird
{
public virtual void Fly() => Console.WriteLine("Flying...");
}
public class Ostrich : Bird
{
public override void Fly()
{
throw new NotSupportedException("Ostriches can’t fly!");
}
}
Now, code expecting any Bird to Fly() breaks with Ostrich.
✅ Fix
public abstract class Bird { }
public class FlyingBird : Bird
{
public virtual void Fly() => Console.WriteLine("Flying...");
}
public class Sparrow : FlyingBird { }
public class Ostrich : Bird { }
Now, you don’t have broken contracts — Ostrich is not forced into Fly().
4. Interface Segregation Principle (ISP)
No client should be forced to implement methods it does not need.
❌ Violation
public interface IMachine
{
void Print();
void Scan();
void Fax();
}
public class OldPrinter : IMachine
{
public void Print() { Console.WriteLine("Printing..."); }
public void Scan() { throw new NotImplementedException(); }
public void Fax() { throw new NotImplementedException(); }
}
Here OldPrinter is forced to implement Scan and Fax which it doesn’t support.
✅ Fix
public interface IPrinter { void Print(); }
public interface IScanner { void Scan(); }
public interface IFax { void Fax(); }
public class OldPrinter : IPrinter
{
public void Print() => Console.WriteLine("Printing...");
}
public class MultiFunctionPrinter : IPrinter, IScanner, IFax
{
public void Print() => Console.WriteLine("Printing...");
public void Scan() => Console.WriteLine("Scanning...");
public void Fax() => Console.WriteLine("Faxing...");
}
Now clients implement only what they need.
5. Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
❌ Violation
public class FileLogger
{
public void Log(string message)
{
File.AppendAllText("log.txt", message);
}
}
public class UserService
{
private readonly FileLogger logger = new FileLogger();
public void RegisterUser(string username)
{
// business logic
logger.Log("User registered: " + username);
}
}
UserService depends on FileLogger (a concrete class). If tomorrow you need DB logging, you must modify UserService.
✅ Fix
public interface ILogger
{
void Log(string message);
}
public class FileLogger : ILogger
{
public void Log(string message) =>
File.AppendAllText("log.txt", message);
}
public class DatabaseLogger : ILogger
{
public void Log(string message) =>
Console.WriteLine($"DB log: {message}");
}
public class UserService
{
private readonly ILogger logger;
public UserService(ILogger logger)
{
this.logger = logger;
}
public void RegisterUser(string username)
{
// business logic
logger.Log("User registered: " + username);
}
}
Now, by injecting a different ILogger, you can switch logging without changing UserService.
🎯 Summary (for interviews)
- SRP: One reason to change. (Separate responsibilities)
- OCP: Extend without modifying existing code. (Polymorphism/specifications)
- LSP: Subtypes should respect base contracts.
- ISP: Favor many small, specific interfaces over one fat interface.
- DIP: Depend on abstractions, not concretes.