SOLID Design Principles in C# [Before & After]

Zied Rebhi
4 min readOct 7, 2023

--

SOLID Desing Principles in C#
SOLID Design Principles in C# [Before & After]

In the world of software development, writing code is just the beginning; crafting code that’s maintainable and adaptable over time is where true craftsmanship lies. That’s where SOLID principles come in. These five fundamental principles offer a blueprint for cleaner and more efficient code design.

In this article, we’ll explore SOLID principles through practical examples. We’ll take a simple code snippet, apply SOLID principles, and witness the transformation from a tangled mess to an elegant and maintainable solution. By the end, you’ll grasp how these principles can revolutionize your approach to software design.

So, let’s dive into the before-and-after of SOLID principles and see how they can make your code not only work but work beautifully.

1. Single Responsibility Principle (SRP)

A class should have only one reason to change.

Before SRP:

public class Customer
{
public string Name { get; set; }
public void AddToDatabase()
{
// Code to add customer to the database
}
public void GenerateInvoice()
{
// Code to generate an invoice
}
}

In the code above, the Customer class has multiple responsibilities: managing customer data and generating invoices. This violates the SRP.

After SRP:

public class Customer
{
public string Name { get; set; }
}

public class CustomerRepository
{
public void AddToDatabase(Customer customer)
{
// Code to add customer to the database
}
}

public class InvoiceGenerator
{
public void GenerateInvoice(Customer customer)
{
// Code to generate an invoice
}
}

In the updated code, we’ve separated the responsibilities into distinct classes: Customer now only represents customer data, CustomerRepository handles database interactions, and InvoiceGenerator takes care of invoice generation. Each class now has a single responsibility.

2. Open-Closed Principle (OCP)

Software entities (classes, modules, functions) should be open for extension but closed for modification.

Before OCP:

public class Rectangle
{
public double Width { get; set; }
public double Height { get; set; }
}

public class AreaCalculator
{
public double CalculateArea(Rectangle rectangle)
{
return rectangle.Width * rectangle.Height;
}
}

In this example, if we want to add support for calculating the area of other shapes, we would need to modify the AreaCalculator class, violating the OCP.

After OCP:

public abstract class Shape
{
public abstract double CalculateArea();
}

public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }

public override double CalculateArea()
{
return Width * Height;
}
}

public class Circle : Shape
{
public double Radius { get; set; }

public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}

In the updated code, we’ve introduced an abstract Shape class and derived classes for specific shapes. Now, to add support for new shapes, you can create new classes that inherit from Shape without modifying existing code, adhering to the OCP.

3. Liskov Substitution Principle (LSP)

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

Before LSP:

public class Bird
{
public virtual void Fly()
{
Console.WriteLine("A bird can fly.");
}
}

public class Ostrich : Bird
{
public override void Fly()
{
Console.WriteLine("An ostrich cannot fly.");
}
}

In this example, the Ostrich class is a subtype of Bird, but it violates the LSP because it alters the behavior of the Fly method.

After LSP:

public interface IFlyable
{
void Fly();
}

public class Bird : IFlyable
{
public void Fly()
{
Console.WriteLine("A bird can fly.");
}
}

public class Ostrich : IFlyable
{
public void Fly()
{
Console.WriteLine("An ostrich cannot fly.");
}
}

In the updated code, we’ve introduced an IFlyable interface that both Bird and Ostrich implement. Now, both classes adhere to the Liskov Substitution Principle, as they can be substituted for each other without altering program correctness.

4. Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they do not use.

Before ISP:

public interface IWorker
{
void Work();
void Eat();
void Sleep();
}

public class Engineer : IWorker
{
public void Work()
{
// Implement work for an engineer
}

public void Eat()
{
// Implement eating for an engineer
}

public void Sleep()
{
// Implement sleeping for an engineer
}
}

In this example, the IWorker interface contains methods that not all clients (e.g., Engineer) need, violating ISP.

After ISP:

public interface IWorker
{
void Work();
}

public interface IEater
{
void Eat();
}

public interface ISleeper
{
void Sleep();
}

public class Engineer : IWorker, IEater, ISleeper
{
public void Work()
{
// Implement work for an engineer
}

public void Eat()
{
// Implement eating for an engineer
}

public void Sleep()
{
// Implement sleeping for an engineer
}
}

In the updated code, we’ve split the IWorker interface into separate interfaces (IEater and ISleeper ) to follow ISP. Now, clients like Engineer can implement only the interfaces they need, preventing unnecessary dependencies.

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.

Before DIP:

public class LightBulb
{
public void TurnOn()
{
// Code to turn on the light bulb
}
}

public class Switch
{
private readonly LightBulb _bulb;

public Switch()
{
_bulb = new LightBulb();
}

public void Flip()
{
_bulb.TurnOn();
}
}

In this example, the Switch class directly depends on the concrete LightBulb class, violating DIP.

After DIP:

public interface ISwitchable
{
void TurnOn();
}

public class LightBulb : ISwitchable
{
public void TurnOn()
{
// Code to turn on the light bulb
}
}

public class Switch
{
private readonly ISwitchable _device;

public Switch(ISwitchable device)
{
_device = device;
}

public void Flip()
{
_device.TurnOn();
}
}

In the updated code, we’ve introduced the ISwitchable interface, allowing Switch to depend on an abstraction rather than a concrete implementation. This adheres to the Dependency Inversion Principle.

--

--