Applying SOLID Programming in C#

SOLID principles stand at the core of crafting maintainable and scalable software in object-oriented programming. This acronym represents five key design principles:

  1. Single Responsibility
  2. Open-Closed
  3. Liskov Substitution
  4. Interface Segregation
  5. Dependency Inversion

Each offers a path towards software that’s easier to debug, understand, and extend, especially within C# development. For beginners in C#, embracing SOLID can dramatically improve code quality, fostering flexible and modular systems that are less prone to common issues like tight coupling.

However, it can be hard for new-comers to make sense of these principles. I know that I certainly did, when I first started out. And I found it difficult to find simple explanations (and examples) of what the principles mean.

This mini-essay will provide both.

S – Single Responsibility Principle (SRP)

SRP Separates the Chef from the Waitstaff

Think of a chef in a restaurant. Their main responsibility is to prepare your meal. It’s not their job to take your order or bill you; those are responsibilities for the waiter and the cashier, respectively.

A piece of code (like a class or function) should do one thing only. If it’s handling more than one task, it becomes complicated, and changing one task might affect the others unintentionally.

// Each class has a single responsibility...

// The Chef cooks a meal, but doesn't take orders
class Chef
{
    public void CookMeal() 
    {
        Console.WriteLine("Cooking meal...");
    }
}

// The Waiter takes orders, but doesn't cook the meal
class Waiter
{
    public void TakeOrder() 
    {
        Console.WriteLine("Taking order...");
    }
}

O – Open/Closed Principle (OCP)

OCP is Like Adding Apps to a Smart Watch

Imagine you have a digital watch that can also be updated with new features without opening it up physically. You can add new functionalities like a stopwatch or a heart rate monitor through software updates.

Your code should be open to adding new features but closed for modification. This means you can extend its behavior without changing the existing code, which helps in maintaining and scaling the application

// The base class is open for extension but closed for modification.
public abstract class Watch
{
    public abstract void ShowTime();
}

// New functionality can be added via extending the base class  - by inheriting from it, 
// and adding the functionality we need
public class SmartWatch : Watch
{
    public override void ShowTime()
    {
        Console.WriteLine("Showing time...");
    }

   // This method is not in the base class, but can be added here
    public void ShowHeartRate()
    {
        Console.WriteLine("Showing heart rate...");
    }
}

L – Liskov Substitution Principle (LSP)

LSP Promises That Your Universal Remote Will Work with All TVs

Think of a universal remote control for TVs. Regardless of what brand of TV you buy, the universal remote should be able to control it.

If you have a piece of code expecting a certain type (like a class), you should be able to substitute it with a subtype (a subclass or an implementation of an interface) without affecting the behavior of your program.

// Here is our Universal Remote Control
public class RemoteControl
{
    // As long as it's trying to control a TV it should work, regardless of brand
    public void ControlDevice(TV tv) 
    {
        tv.TurnOn();
    }
}

// Here's our base class
public class TV
{
    public virtual void TurnOn()
    {
        Console.WriteLine("TV turned on.");
    }
}

// Since RemoteControl works with TVs, and MyBrandTV is a TV, it should work
public class MyBrandTV: TV
{
    public override void TurnOn()
    {
        Console.WriteLine("MyBrandTV turned on with additional features.");
    }
}

RemoteControl remote = new RemoteControl();

TV myTv = new MyBrandTV (); // Here is the Liskov Substitution Principle in action
remote.ControlDevice(myTv); // It works!

I – Interface Segregation Principle (ISP)

ISP Means Having the Right Tool for the Right Job

Imagine you order a meal at a restaurant and, rather than giving you a a spoon for your soup, a fork for your salad, and a knife to help with your steak – they just give you single spork with sharp edges. Can it work for soup, and salad, and steak? Maybe, but it’s not going to do any of them particularly well.

Sometimes in program we do something similar. We try to make catch-all solutions.

ISP teaches us not to force a piece of code to depend on functionality that it doesn’t use. Instead, split big, generalized interfaces into smaller, more specific ones so that the code only needs to know about the methods it actually uses.

// Instead of a single, fat interface, we have smaller specific interfaces...

// A spoon for soup
public interface ISpoon
{
    void EatSoup();
}

// A fork for salad and steak
public interface IFork
{
    void EatSalad();
    void EatSteak();
}

// A knife for cutting the steak
public interface IKnife
{
    void CutSteak();
}

// Now we implement the interfaces:
public class Spoon : ISpoon
{
    public void EatSoup()
    {
        Console.WriteLine("Eating soup with a spoon.");
    }
}

public class Fork : IFork
{
    public void EatSalad()
    {
        Console.WriteLine("Eating salad with a fork.");
    }

    public void EatSteak()
    {
        Console.WriteLine("Eating steak with a fork.");
    }
}

public class Knife: IKnife
{
    public void CutSteak()
    {
        Console.WriteLine("Cutting steak with a knife.");
    }
}

D – Dependency Inversion Principle (DIP)

DIP is Like a Grocery Delivery Service

Instead of going to the supermarket yourself every time you need groceries, let’s say you subscribe to a delivery service. Now, you don’t care about which specific supermarket your groceries come from; you’re just concerned about getting what you need.

DIP states that high-level modules (the broad tasks your application performs) should not depend on low-level modules (the specific implementations of tasks). Both should depend on abstractions (interfaces or abstract classes), making it easier to change the low-level implementations without affecting the high-level modules.

// Here's a grocery delivery service
public interface IGroceryDeliveryService
{
    void DeliverGroceries();
}

// Here's our kitchen
public class MyKitchen
{
    // Here we're saying that we want a delivery service
    private IGroceryDeliveryService _deliveryService;
    
    // Here's where we actually get a specific service, maybe "My Brand Delivery Service"
    public MyKitchen(IGroceryDeliveryService deliveryService)
    {
        _deliveryService = deliveryService;
    }

    // Now we can get groceries from the service, not caring how it works behind the scenes
    public void GetGroceries()
    {
        _deliveryService.DeliverGroceries();
    }
}

// MyBrandDeliveryService is a type of Grocery Delivery Service
public class MyBrandDeliveryService: IGroceryDeliveryService
{
    // It's job is to deliver groceries
    public void DeliverGroceries()
    {
        Console.WriteLine("Delivering groceries from *any* supermarket.");
    }
}

// Here's our Delivery Service 
IGroceryDeliveryService deliveryService = new MyBrandDeliveryService();

// Now we've subscribed to it!
MyKitchen kitchen = new MyKitchen(deliveryService);

// And now we're getting groceries straight from the kitchen
kitchen.GetGroceries();

Conclusion

Adopting SOLID principles in C# transforms good coding into great software. By guiding you with examples, from chefs to smart watches, we’ve shown how each principle can simplify and strengthen your work. These aren’t just rules; they’re tools to make your code clear, flexible, and robust.

Starting with SOLID might feel overwhelming for new programmers, but incorporating these concepts gradually will immensely benefit your projects. Think of SOLID as a compass in the complex world of programming, steering you towards better practices. With every line of code aligned with SOLID, you’re building a stronger foundation for your development career. Embrace these principles, and watch your code—and your skills—evolve.