Mastering SOLID Principles in C# with Real-Time Examples

Introduction

The SOLID principles are a set of five design principles intended to make software designs more understandable, flexible, and maintainable. These principles were introduced by Robert C. Martin and are widely recognized in the object-oriented programming community. Let's explore each principle with a real-time example in C#.

1. Single Responsibility Principle (SRP)

A class should have only one reason to change, meaning it should have only one job or responsibility.

Example. Let's consider a Report class that handles generating and printing reports. According to SRP, these responsibilities should be separated.

Bad Example (Violating SRP)

public class Report
{
    public string GetReport()
    {
        // Generate report
        return "Report data";
    }
    public void PrintReport(string report)
    {
        // Print report
        Console.WriteLine(report);
    }
}

Refactored Example (Following SRP)

public class ReportGenerator
{
    public string GetReport()
    {
        // Generate report
        return "Report data";
    }
}
public class ReportPrinter
{
    public void PrintReport(string report)
    {
        // Print report
        Console.WriteLine(report);
    }
}
// Usage
var reportGenerator = new ReportGenerator();
var reportPrinter = new ReportPrinter();
string report = reportGenerator.GetReport();
reportPrinter.PrintReport(report);

2. Open/Closed Principle (OCP)

Software entities should be open for extension but closed for modification.

Example. Consider a class that calculates the area of different shapes. We want to extend this functionality without modifying existing code.

Bad Example (Violating OCP)

public class AreaCalculator
{
    public double CalculateArea(object shape)
    {
        if (shape is Circle)
        {
            var circle = (Circle)shape;
            return Math.PI * circle.Radius * circle.Radius;
        }
        else if (shape is Rectangle)
        {
            var rectangle = (Rectangle)shape;
            return rectangle.Width * rectangle.Height;
        }
        return 0;
    }
}

Refactored Example (Following OCP)

public abstract class Shape
{
    public abstract double CalculateArea();
}
public class Circle : Shape
{
    public double Radius { get; set; }

    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
}
public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }

    public override double CalculateArea()
    {
        return Width * Height;
    }
}
public class AreaCalculator
{
    public double CalculateArea(Shape shape)
    {
        return shape.CalculateArea();
    }
}
// Usage
var circle = new Circle { Radius = 5 };
var rectangle = new Rectangle { Width = 4, Height = 6 };
var calculator = new AreaCalculator();
Console.WriteLine($"Circle Area: {calculator.CalculateArea(circle)}");
Console.WriteLine($"Rectangle Area: {calculator.CalculateArea(rectangle)}");

3. Liskov Substitution Principle (LSP)

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

Example. Let's ensure that derived classes can replace base classes without altering the program behavior.

Bad Example (Violating LSP)

public class Bird
{
    public virtual void Fly()
    {
        // Implementation for flying
    }
}
public class Ostrich : Bird
{
    public override void Fly()
    {
        throw new NotImplementedException();
    }
}

Refactored Example (Following LSP)

public abstract class Bird
{
    public abstract void Move();
}
public class Sparrow : Bird
{
    public override void Move()
    {
        Fly();
    }

    private void Fly()
    {
        // Implementation for flying
    }
}
public class Ostrich : Bird
{
    public override void Move()
    {
        Run();
    }

    private void Run()
    {
        // Implementation for running
    }
}
// Usage
Bird sparrow = new Sparrow();
Bird ostrich = new Ostrich();
sparrow.Move(); // Flies
ostrich.Move(); // Runs

4. Interface Segregation Principle (ISP)

No client should be forced to depend on methods it does not use.

Example. Consider a large interface that imposes methods on classes that don't need them.

Bad Example (Violating ISP)

public interface IWorker
{
    void Work();
    void Eat();
}
public class HumanWorker : IWorker
{
    public void Work()
    {
        // Working
    }

    public void Eat()
    {
        // Eating
    }
}
public class RobotWorker : IWorker
{
    public void Work()
    {
        // Working
    }

    public void Eat()
    {
        throw new NotImplementedException();
    }
}

Refactored Example (Following ISP)

public interface IWorkable
{
    void Work();
}
public interface IFeedable
{
    void Eat();
}
public class HumanWorker : IWorkable, IFeedable
{
    public void Work()
    {
        // Working
    }

    public void Eat()
    {
        // Eating
    }
}
public class RobotWorker : IWorkable
{
    public void Work()
    {
        // Working
    }
}
// Usage
IWorkable humanWorker = new HumanWorker();
IWorkable robotWorker = new RobotWorker();
humanWorker.Work();
robotWorker.Work();

5. Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.

Example. Consider a high-level class that depends on low-level class details.

Bad Example (Violating DIP)

public class EmailService
{
    public void SendEmail(string message)
    {
        // Send email logic
    }
}
public class Notification
{
    private EmailService _emailService;

    public Notification()
    {
        _emailService = new EmailService();
    }

    public void Send(string message)
    {
        _emailService.SendEmail(message);
    }
}

Refactored Example (Following DIP)

public interface IMessageService
{
    void SendMessage(string message);
}
public class EmailService : IMessageService
{
    public void SendMessage(string message)
    {
        // Send email logic
    }
}
public class Notification
{
    private readonly IMessageService _messageService;

    public Notification(IMessageService messageService)
    {
        _messageService = messageService;
    }

    public void Send(string message)
    {
        _messageService.SendMessage(message);
    }
}
// Usage
IMessageService emailService = new EmailService();
var notification = new Notification(emailService);
notification.Send("Hello, Dependency Inversion!");

Conclusion

The SOLID principles are essential for creating robust, maintainable, and scalable software. By adhering to these principles, developers can avoid common pitfalls and ensure their code is clean and manageable.

By understanding and applying the SOLID principles, developers can significantly improve the quality of their code and the success of their projects.


Similar Articles