DEV Community

Cover image for Understanding SOLID Principles in C#
Odumosu Matthew
Odumosu Matthew

Posted on

Understanding SOLID Principles in C#

As developers, we often come across the term "SOLID" principles, which are a set of five design principles that help us write maintainable, scalable, and efficient code. However, understanding and applying these concepts in real-life projects can sometimes be tricky. To make it simpler, let's break down each principle with practical, relatable examples and code snippets that any developer can understand.

1. Single Responsibility Principle (SRP)

Real-Life Example:

Imagine you’re organizing a community event, and Alex, your friend, tries to handle everything—from setting up the venue, to promoting the event, to managing food. With so many responsibilities, things often slip through the cracks. If Alex focused on just one task, like promotion, they could do it efficiently.

Tech Takeaway:

In software development, SRP suggests that every class or function should have only one responsibility. This makes your code easier to maintain, test, and debug.

Code Example:

// Bad Example: A single class doing multiple tasks
public class EventManager
{
    public void OrganizeEvent() { /* venue setup */ }
    public void PromoteEvent() { /* promotion logic */ }
    public void ManageFood() { /* food management */ }
}

// Good Example: Separate responsibilities into different classes
public class VenueSetup { public void OrganizeEvent() { /* venue setup */ } }
public class EventPromotion { public void PromoteEvent() { /* promotion logic */ } }
public class FoodManagement { public void ManageFood() { /* food management */ } }

Enter fullscreen mode Exit fullscreen mode

By breaking up the EventManager class, we now have clear responsibilities for each class. This makes future changes easier to implement and manage.

2. Open/Closed Principle (OCP)

Real-Life Example:

Let’s say you’re hosting the same event, and Alex is responsible for creating the agenda. If every time you invite a new speaker, Alex has to rewrite the entire agenda from scratch, it becomes frustrating and time-consuming. But if Alex has a system that allows for adding new speakers without modifying the existing agenda, it’s much easier to manage.

Tech Takeaway:

In programming, OCP states that a class should be open for extension but closed for modification. You can add new functionality without changing the existing code.

Code Example:

// Bad Example: Modifying the method for every new speaker type
public class Agenda
{
    public string GetAgenda(string speakerType)
    {
        if (speakerType == "Motivational") return "Motivational Speaker Agenda";
        if (speakerType == "Technical") return "Technical Speaker Agenda";
        // More speaker types...
    }
}

// Good Example: Use inheritance to extend without modifying existing code
public abstract class Speaker
{
    public abstract string GetAgenda();
}

public class MotivationalSpeaker : Speaker
{
    public override string GetAgenda() { return "Motivational Speaker Agenda"; }
}

public class TechnicalSpeaker : Speaker
{
    public override string GetAgenda() { return "Technical Speaker Agenda"; }
}

Enter fullscreen mode Exit fullscreen mode

In the good example, we can easily add new types of speakers without changing the existing Speaker class. This follows the OCP principle, allowing the system to grow without breaking existing functionality.

3. Liskov Substitution Principle (LSP)

Real-Life Example:

Imagine you have both a magician and a motivational speaker at your event. While a magician may perform tricks, a motivational speaker cannot. If you expect both speakers to perform magic, it will lead to failure.

Tech Takeaway:
In programming, LSP means that objects of a derived class should be able to replace objects of the base class without affecting the correctness of the program. In other words, subclasses should behave as their parent class promises.

Code Example:

// Bad Example: Forcing all speakers to perform magic
public class Speaker
{
    public virtual void Perform() { /* perform magic */ }
}

public class MotivationalSpeaker : Speaker
{
    public override void Perform() { throw new NotImplementedException(); }
}

// Good Example: Separate behaviors based on the type of speaker
public abstract class Speaker
{
    public abstract void Speak();
}

public class Magician : Speaker
{
    public void PerformMagic() { /* perform magic tricks */ }
}

public class MotivationalSpeaker : Speaker
{
    public override void Speak() { /* motivational speech */ }
}

Enter fullscreen mode Exit fullscreen mode

Now, motivational speakers are not forced to perform magic, ensuring that any speaker can be substituted without issues.

4. Interface Segregation Principle (ISP)

Real-Life Example:

Alex creates a massive checklist that covers every possible task—from chair arrangement to social media promotion. But volunteers only need to see the tasks they’re responsible for, like setting up the chairs or handling snacks.

Tech Takeaway:
In software, ISP suggests that instead of having a single large interface, you should split it into smaller, more specific interfaces so that classes only implement the methods they actually need.

Code Example:

// Bad Example: A single large interface
public interface IEventTasks
{
    void SetUpChairs();
    void PromoteEvent();
    void ManageSnacks();
}

// Good Example: Smaller, more specific interfaces
public interface IChairSetup
{
    void SetUpChairs();
}

public interface IPromotion
{
    void PromoteEvent();
}

public interface ISnackManagement
{
    void ManageSnacks();
}

Enter fullscreen mode Exit fullscreen mode

Now, volunteers can only take responsibility for specific tasks without being overwhelmed by unrelated methods they don’t need.

5. Dependency Inversion Principle (DIP)

Real-Life Example:

Alex relies heavily on a single supplier for food, chairs, and decorations. If that supplier fails to deliver, the entire event is at risk. If Alex had multiple suppliers, the event could still proceed smoothly.

Tech Takeaway:
DIP recommends that high-level modules should not depend on low-level modules, but both should depend on abstractions (like interfaces). This allows us to swap out dependencies easily when needed.

Code Example:

// Bad Example: Hardcoding the dependency
public class EventOrganizer
{
    private CateringService _cateringService = new CateringService();
    public void Organize() { _cateringService.ProvideFood(); }
}

// Good Example: Use abstraction to make the dependency flexible
public interface ICateringService
{
    void ProvideFood();
}

public class EventOrganizer
{
    private ICateringService _cateringService;
    public EventOrganizer(ICateringService cateringService)
    {
        _cateringService = cateringService;
    }

    public void Organize() { _cateringService.ProvideFood(); }
}

Enter fullscreen mode Exit fullscreen mode

Now, the EventOrganizer class is flexible. You can switch to a different catering service without changing the EventOrganizer code, thanks to the abstraction.

Conclusion:
SOLID principles help us write cleaner, more maintainable, and scalable code by encouraging good design practices. Whether you're handling a simple event or building complex software, following these principles ensures that your application remains adaptable and robust over time. Just like in event planning, breaking responsibilities into manageable tasks and maintaining flexibility ensures everything runs smoothly!

LinkedIn Account : LinkedIn
Twitter Account: Twitter
Credit: Graphics sourced from LinkedIn

Top comments (0)