The SOLID principles are five simple rules that make object-oriented code easier to read, maintain, and extend. Let’s break them down with examples.
A class should have only one reason to change. Do one thing, do it well.
// ❌ Bad: Handles both saving and logging
class OrderService
{
public void SaveOrder(string order) { /* save */ }
public void Log(string message) { /* log */ }
}
// ✅ Good: Separate concerns
class OrderRepository
{
public void Save(string order) { /* save */ }
}
class Logger
{
public void Log(string message) { /* log */ }
}
Software should be open for extension but closed for modification. Add new code without changing existing code.
// ❌ Bad: Adding new shapes requires editing the class
class AreaCalculator
{
public double Area(object shape)
{
if (shape is Circle c) return Math.PI * c.Radius * c.Radius;
if (shape is Square s) return s.Side * s.Side;
return 0;
}
}
// ✅ Good: Use abstraction (polymorphism)
interface IShape { double Area(); }
class Circle : IShape { public double Radius { get; set; } public double Area() => Math.PI * Radius * Radius; }
class Square : IShape { public double Side { get; set; } public double Area() => Side * Side; }
class AreaCalculator
{
public double Area(IShape shape) => shape.Area();
}
Subclasses should be usable wherever their base class is expected. No surprises.
// ❌ Bad: Square breaks Rectangle behavior
class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int Area() => Width * Height;
}
class Square : Rectangle
{
public override int Width { set { base.Width = base.Height = value; } }
public override int Height { set { base.Width = base.Height = value; } }
}
// ✅ Good: Use separate abstractions
interface IShape { int Area(); }
class RectangleShape : IShape { public int Width { get; set; } public int Height { get; set; } public int Area() => Width * Height; }
class SquareShape : IShape { public int Side { get; set; } public int Area() => Side * Side; }
Don’t force classes to implement things they don’t need. Smaller, specific interfaces are better than large, general ones.
// ❌ Bad: One big interface
interface IMachine
{
void Print();
void Scan();
void Fax();
}
class OldPrinter : IMachine
{
public void Print() { }
public void Scan() { throw new NotImplementedException(); }
public void Fax() { throw new NotImplementedException(); }
}
// ✅ Good: Smaller interfaces
interface IPrinter { void Print(); }
interface IScanner { void Scan(); }
class SimplePrinter : IPrinter
{
public void Print() { }
}
Depend on abstractions, not concrete classes. High-level modules shouldn’t depend on low-level modules directly.
// ❌ Bad: High-level depends on low-level
class FileLogger
{
public void Log(string message) { /* write to file */ }
}
class OrderService
{
private FileLogger _logger = new FileLogger();
public void Save(string order) { _logger.Log("Saved order"); }
}
// ✅ Good: Depend on abstraction
interface ILogger { void Log(string message); }
class FileLogger : ILogger { public void Log(string msg) { /* file */ } }
class ConsoleLogger : ILogger { public void Log(string msg) { Console.WriteLine(msg); } }
class OrderService
{
private readonly ILogger _logger;
public OrderService(ILogger logger) { _logger = logger; }
public void Save(string order) { _logger.Log("Saved order"); }
}