SOLID - Single Responsiblity Principle in C#

In software development, maintaining clean, scalable, and maintainable code is crucial for the long-term success of any project. To achieve these goals, software engineers often turn to design principles such as SOLID, a set of five principles aimed at improving the design of object-oriented systems Among these principles lies the Single Responsibility Principle (SRP), which serves as a cornerstone for writing robust and maintainable code.

What is the Single Responsibility Principle?

At its core, the Single Responsibility Principle (SRP) states that a class should have only one reason to change. In other words, a class should have a single responsibility or encapsulate only one aspect of the software’s functionality. This principle promotes cohesion and reduces coupling within the codebase, making classes more modular, easier to understand, and less prone to bugs.

Example: Violating SRP

Now that you know what SRP is, let’s see what code looks like when it’s not used. In this example, the UserManager has multiple responsibilites. It’s doing the following:

Consider a UserManager class that undertakes multiple responsibilities, including user registration, username and email validation, saving users to a database, and sending email confirmations. Initially, this approach might seem manageable, but it quickly becomes unwieldy as the class expands to encompass more functionality, potentially growing to thousands of lines of code.

public class UserManager
{
    public void RegisterUser(string username, string email)
    {
        if (ValidateUsername(username) && ValidateEmail(email))
        {
            SaveUserToDatabase(username, email);
            SendEmailConfirmation(email);
        }
    }

    private bool ValidateUsername(string username)
    {
        // Validation logic for username
        return !string.IsNullOrEmpty(username);
    }

    private bool ValidateEmail(string email)
    {
        // Validation logic for email
        return email.Contains("@");
    }

    private void SaveUserToDatabase(string username, string email)
    {
        // Database saving logic
        Console.WriteLine($"Saving user {username} with email {email} to the database...");
    }

    private void SendEmailConfirmation(string email)
    {
        // Email sending logic
        Console.WriteLine($"Sending confirmation email to {email}...");
    }
}

Refactoring Towards SRP

To align with SRP, it’s beneficial to refactor this monolithic class into several classes, each dedicated to a specific responsibility. Break out your refactoring hammer, and let’s get to work!

Here’s how you can undertake this process:

  1. Validation Logic: Extract username and email validation into a separate UserValidator class.
  2. Database Interaction: Isolate the database saving logic into a UserRepository class.
  3. Email Communication: Delegate the responsibility of sending email confirmations to an EmailService class.

By distributing responsibilities across UserValidator, UserRepository, and EmailService, we significantly improve the modularity and maintainability of the code. The revised UserManager class now coordinates these components to register users.

Let’s see what this might look like:

public class UserValidator
{
    public bool ValidateUsername(string username)
    {
        // Validation logic for username
        return !string.IsNullOrEmpty(username);
    }

    public bool ValidateEmail(string email)
    {
        // Validation logic for email
        return email.Contains("@");
    }
}
public class UserRepository
{
    public void SaveUser(string username, string email)
    {
        // Database saving logic
        Console.WriteLine($"Saving user {username} with email {email} to the database...");
    }
}
public class EmailService
{
    public void SendEmailConfirmation(string email)
    {
        // Email sending logic
        Console.WriteLine($"Sending confirmation email to {email}...");
    }
}

We can now create instances of these new classes in our main UserManager class and call out for the extracted functionality.

public class UserManager
{
    private readonly UserValidator _validator;
    private readonly UserRepository _repository;
    private readonly EmailService _emailService;

    public UserManager()
    {
        _validator = new UserValidator();
        _repository = new UserRepository();
        _emailService = new EmailService();
    }

    public void RegisterUser(string username, string email)
    {
        if (_validator.ValidateUsername(username) && _validator.ValidateEmail(email))
        {
            _repository.SaveUser(username, email);
            _emailService.SendEmailConfirmation(email);
        }
    }
}

Boom! Our main UserManager class has been broken apart into smaller classes. This will be easier to maintain going forward.

Should You Always Follow It?

In most cases, it’s good practice to follow the Single Responsibility Principle. There are times where it’s ok to skip it though. For example, when you’re building a proof of concept, or a rapid prototype. It’s fine to leave this principle behind during this stage, just make sure to go back and refactor before your code is merged into the main branch.

Recent Posts

SOLID - Dependency Inversion Principle (DIP) in C#
SOLID - Interface Segregation Principle (ISP) in C#
SOLID - Liskov Substitution Principle (LSP) in C#
SOLID - Open/Closed Principle (OCP) in C#
SOLID - Single Responsiblity Principle (SRP) in C#
How to Make a CLI Menu in C#