SOLID Principles — Easy C# Guide

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.

1) S — Single Responsibility Principle

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 */ }
}

2) O — Open/Closed Principle

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();
}

3) L — Liskov Substitution Principle

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; }

4) I — Interface Segregation Principle

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() { }
}

5) D — Dependency Inversion Principle

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"); }
}