Skip to main content

Command Palette

Search for a command to run...

Mastering SOLID Principles with C#: Building Maintainable Code

Updated
•4 min read•View as Markdown
Mastering SOLID Principles with C#: Building Maintainable Code
I

Experienced Computer System Engineer with over 10 years of industry expertise in various roles such as developer, code reviewer, architect, and team lead. Successfully delivered more than 20 projects while leveraging a diverse skill set and a deep understanding of software development methodologies. Proficient in technologies including .NET, C#, Xamarin Forms, Xamarin Native, MAUI, HTML5, CSS3, JavaScript, TypeScript, Angular, React, Azure, SCRUM, and SQL.

Introduction: SOLID is an acronym representing five principles that assist developers in building maintainable and scalable systems. In this tutorial, we will explore the SOLID principles with practical C# examples.

S - Single Responsibility Principle (SRP): SRP states that a class should have only one reason to change. This ensures that a class is focused on what it should do and does not take on responsibilities that it shouldn't.

Example:

public class Logger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}

public class Calculator
{
    private readonly Logger _logger;

    public Calculator(Logger logger)
    {
        _logger = logger;
    }

    public int Add(int a, int b)
    {
        _logger.Log("Adding numbers.");
        return a + b;
    }
}

Here, the Calculator class is only concerned with calculations and the Logger the class handles logging.

O - Open/Closed Principle (OCP): OCP states that a class should be open for extension but closed for modification. This can be achieved by using interfaces, abstract classes, or virtual methods.

Example:

public interface IShape
{
    double Area();
}

public class Rectangle : IShape
{
    public double Width { get; set; }
    public double Height { get; set; }
    public double Area()
    {
        return Width * Height;
    }
}

public class Circle : IShape
{
    public double Radius { get; set; }
    public double Area()
    {
        return Math.PI * Radius * Radius;
    }
}

L - Liskov Substitution Principle (LSP): The Liskov Substitution Principle (LSP) states that objects of a superclass should be able to be replaced with objects of a subclass without affecting the correctness of the program. In simple terms, if class B is a subclass of class A, then we should be able to replace A with B without disrupting the behavior of the program.

One common scenario where LSP is violated involves overriding the base class methods in a way that's not compatible with the behavior expected from the base class.

Example: Let's consider an example involving shapes:

public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }

    public int Area()
    {
        return Width * Height;
    }
}

public class Square : Rectangle
{
    private int _side;

    public override int Width
    {
        get { return _side; }
        set { _side = value; }
    }

    public override int Height
    {
        get { return _side; }
        set { _side = value; }
    }
}

This example involves a Rectangle class and a Square class that inherits from Rectangle. It seems logical, as a square is a special kind of rectangle. However, this violates the Liskov Substitution Principle. A square has all sides equal, so setting either the width or the height should set both, while a rectangle doesn't have this constraint.

If you use a Square object in a context where a Rectangle object is expected, this could lead to unexpected behavior.

csharpCopy codeRectangle rectangle = new Square();
rectangle.Width = 5;
rectangle.Height = 10;
Console.WriteLine(rectangle.Area()); // This will output 100 instead of 50.

Solution: To comply with LSP, we could use a more general type for both classes, like Polygon, and then use separate properties and methods that make sense for each derived class without making assumptions.

public interface IPolygon
{
    int Area();
}

public class Rectangle : IPolygon
{
    public int Width { get; set; }
    public int Height { get; set; }

    public int Area()
    {
        return Width * Height;
    }
}

public class Square : IPolygon
{
    public int Side { get; set; }

    public int Area()
    {
        return Side * Side;
    }
}

In this example, both Rectangle and Square implement the IPolygon interface. This approach respects the Liskov Substitution Principle and avoids unexpected behavior.

I - Interface Segregation Principle (ISP): ISP states that a class should not be forced to implement interfaces it doesn't use. Interfaces should be small and focused.

Example:

public interface IPrinter
{
    void Print();
}

public interface IFax
{
    void Fax();
}

public class Printer : IPrinter
{
    public void Print()
    {
        // print logic
    }
}

D - Dependency Inversion Principle (DIP): DIP encourages dependence on abstractions rather than concretions. This makes the system more flexible and adaptive to change.

Example:

public interface IMessageSender
{
    void SendMessage(string message);
}

public class EmailSender : IMessageSender
{
    public void SendMessage(string message)
    {
        // Send email
    }
}

public class Notification
{
    private readonly IMessageSender _messageSender;

    public Notification(IMessageSender messageSender)
    {
        _messageSender = messageSender;
    }

    public void Notify(string message)
    {
        _messageSender.SendMessage(message);
    }
}

Conclusion: The SOLID principles are essential for creating maintainable, scalable, and robust systems. With practical application in C#, you can write cleaner and more efficient code that stands the test of time.