SOLID Design Principles in C# - Single Responsibility

2022, Aug 27

Great developers are always curious and open to learning new programming languages, practices and principles in software development. In this series, I would like to explore the SOLID principles using C#. In this article, the Single Responsibility Principle (SRP) is going to be explored.

This post is part of a series where we explore the SOLID design principles, arguably the most popular design principles for object-oriented software development.

A bit of history

I'm not going to copy and paste all the history about it, you can check the details here, but in summary Robert Cecil Martin (aka Uncle Bob) introduced a paper about Design Principles and Design Patterns, summarised in his article The Principles of OOD, and then Michael Feathers later created the acronym SOLID.

All the principles

In different articles, all the principles will be approached. As a developer, you must learn, not only the acronym meaning but also what they represent and when to use them. These principles will help you to write clean code, with separation of concerns, cohesion, decoupling, helping with code reuse. They are:

  • Single-Responsibility Principle (SRP)
  • Open-Closed Principle (OCP)
  • Liskov Substitution Principle (LSP)
  • Interface Segregation Principle (ISP)
  • Dependency Inversion Principle (DIP)

Single-Responsibility Principle

A class should have one, and only one, reason to change Robert C. Martin

This principle is applied to classes and functions, and they need to have one responsibility, a single purpose.

There are great benefits of applying SRP:

  • Code becomes easier to understand - there is less happening when the implementation is more specific, as the classes are more concise.
  • Code becomes easier to maintain - Less is more, as the classes don't deal with multiple things.
  • Code is changed less frequently - in general, when requirements change, not every part of the application is going to change because the classes are more specific.
  • Code is testable - because classes are specific and small, code can be thoroughly tested.

The wrong example

A classic example of how to violate the Single Responsibility Principle:

using SOLID_SRP.Models;

namespace SOLID_SRP
{
    public class OrderProcessor
    {
        private readonly Order _order;

        public OrderProcessor(Order order)
        {
            _order = order;
        }

        // Validate
        public void Validate()
        {
            //TODO: validate order
            Console.WriteLine($"validate order: {_order.OrderNumber}");
        }

        // Save
        public void Save()
        {
            //TODO: save order
            Console.WriteLine($"save order: {_order.OrderNumber}");
        }

        // Notify
        public void Notify()
        {
            //TODO: send notification
            Console.WriteLine($"send notification: {_order.OrderNumber}");
        }
    }
}

Looking at the implementation above we can see the implementation of many responsibilities: Validate(), Save(), Notify(). This is a violation of SRP.

Problems we can identify with this implementation:

  • No cohesion - There are different things in the same context of OrderProcessor.
  • Coupling - Because of too many responsibilities, we have too many dependencies, leaving OrderProcessor in a fragile state in case changes are needed.
  • Testability - It becomes more difficult to test the implementation.
  • Reusability - Because the code is not modular, code reuse is affected.

The right example

Based on the previous example, the OrderProcessor now becomes an orchestrator, and its responsibilities Validate(), Save(), Notify() are split into their own classes.

using SOLID_SRP.Models;

namespace SOLID_SRP
{
    public class OrderProcessor
    {
        private readonly OrderValidator _orderValidator;
        private readonly OrderSaver _orderSaver;
        private readonly OrderNotifier _orderNotifier;

        public OrderProcessor(OrderValidator orderValidator, OrderSaver orderSaver, OrderNotifier orderNotifier)
        {
            _orderValidator = orderValidator;
            _orderSaver = orderSaver;
            _orderNotifier = orderNotifier;
        }
        
        public void Process()
        {
            _orderValidator.Validate();
            _orderSaver.Save();
            _orderNotifier.Notify();
        }
    }
}

Each class now has its own responsibility:

using SOLID_SRP.Models;

namespace SOLID_SRP
{
    public class OrderValidator
    {
        private readonly Order _order;

        public OrderValidator(Order order)
        {
            _order = order;
        }

        public void Validate()
        {
            //TODO: validate order
            Console.WriteLine($"validate order: {_order.OrderNumber}");
        }
    }
}
using SOLID_SRP.Models;

namespace SOLID_SRP
{
    public class OrderSaver
    {
        private readonly Order _order;

        public OrderSaver(Order order)
        {
            _order = order;
        }

        public void Save()
        {
            //TODO: save order
            Console.WriteLine($"save order: {_order.OrderNumber}");
        }
    }
}
using SOLID_SRP.Models;

namespace SOLID_SRP
{
    public class OrderNotifier
    {
        private readonly Order _order;

        public OrderNotifier(Order order)
        {
            _order = order;
        }

        public void Notify()
        {
            //TODO: send notification
            Console.WriteLine($"send notification: {_order.OrderNumber}");
        }
    }
}

The Single Responsibility example is available on PlayGoKids repository.

Full Series

Single-Responsibility Principle (SRP)

Open-Closed Principle (OCP)

Liskov Substitution Principle (LSP)

Interface Segregation Principle (ISP)

Dependency Inversion Principle (DIP)