SOLID: Single Responsibility Principle(SRP) in C#

Aditya Sharma - Oct 30 - - Dev Community

Introduction

When designing software, adhering to solid principles can make a huge difference in code maintainability, readability, and scalability. The SOLID principles are a set of five design principles meant to guide developers in crafting code that is easier to manage and evolve over time. This article dives into the first of these principles: the Single Responsibility Principle (SRP), with examples in C#.

What is the Single Responsibility Principle?

The Single Responsibility Principle states that a class should have only one reason to change. In simpler terms, it means that a class should only do one job. If a class has more than one reason to change, it is likely trying to handle multiple responsibilities, which can lead to tightly coupled code, reduced flexibility, and difficulties in testing.

Why SRP?

Adhering to SRP offers several benefits:

  1. Improved Readability: When each class has a single responsibility, the purpose of each class is clear, making the code easier to read and understand.
  2. Easier Testing: Smaller, single-responsibility classes are easier to test in isolation.
  3. Reduced Coupling: When each class handles only one responsibility, changes to one part of the code are less likely to impact unrelated parts.
  4. Enhanced Maintainability: With less coupling and clearer responsibilities, it’s easier to maintain and modify the code over time.

An Example of SRP Violation

Let's take a look at an example where the Single Responsibility Principle is violated. Consider a simple Invoice class:

Bad Code ❌

public class Invoice
{
    public void GenerateInvoice() 
    {
        // Code to generate the invoice
    }

    public void SaveToDatabase() 
    {
        // Code to save invoice data to the database
    }

    public void SendEmail() 
    {
        // Code to send the invoice via email
    }
}
Enter fullscreen mode Exit fullscreen mode

At first glance, this class might seem fine, but it’s handling three different responsibilities:

  1. Generating the invoice
  2. Saving the invoice to the database
  3. Sending the invoice via email

Each of these tasks is distinct and could require changes for different reasons. For example, if the email format changes, we would need to modify the SendEmail method. If the database schema changes, we’d need to update the SaveToDatabase method.

Refactoring to Follow SRP

To apply SRP, we should break down the responsibilities into separate classes. Each class should handle only one task, making the code more modular, testable, and easier to maintain.

Step 1: Separate the Responsibilities

We’ll start by breaking down the Invoice class into three separate classes:

  1. InvoiceGenerator: Responsible for generating the invoice.
  2. InvoiceRepository: Handles saving the invoice to the database.
  3. InvoiceEmailSender: Manages the email sending process.

Good Code ✅

public class InvoiceGenerator
{
    public void GenerateInvoice()
    {
        // Code to generate the invoice
    }
}

public class InvoiceRepository
{
    public void SaveToDatabase()
    {
        // Code to save invoice data to the database
    }
}

public class InvoiceEmailSender
{
    public void SendEmail()
    {
        // Code to send the invoice via email
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Coordinating with a Higher-Level Class

If we need a class to orchestrate the process of generating, saving, and emailing the invoice, we can introduce a new InvoiceService class to handle this coordination:

Good Code ✅

public class InvoiceService
{
    private readonly InvoiceGenerator _invoiceGenerator;
    private readonly InvoiceRepository _invoiceRepository;
    private readonly InvoiceEmailSender _invoiceEmailSender;

    public InvoiceService(
        InvoiceGenerator invoiceGenerator, 
        InvoiceRepository invoiceRepository, 
        InvoiceEmailSender invoiceEmailSender)
    {
        _invoiceGenerator = invoiceGenerator;
        _invoiceRepository = invoiceRepository;
        _invoiceEmailSender = invoiceEmailSender;
    }

    public void ProcessInvoice()
    {
        _invoiceGenerator.GenerateInvoice();
        _invoiceRepository.SaveToDatabase();
        _invoiceEmailSender.SendEmail();
    }
}
Enter fullscreen mode Exit fullscreen mode

By organizing the code this way, each class now has a single responsibility:

  1. InvoiceGenerator is focused solely on generating invoices.
  2. InvoiceRepository is concerned only with database operations for invoices.
  3. InvoiceEmailSender handles the email process.

When to Use SRP

Knowing when to apply SRP depends on the specific context. If a class is small and its responsibilities are closely related, it might be fine as is. However, if a class starts to grow or begins handling responsibilities that could change independently, it’s a good time to consider SRP.

Conclusion

The Single Responsibility Principle is fundamental in creating clean, maintainable, and scalable code. By ensuring that each class has only one reason to change, you build software that’s easier to debug, test, and extend. In this example, breaking the Invoice class into distinct, single-purpose classes improved our design, making the application easier to maintain.

Following SRP, along with the other SOLID principles, can transform how you design and maintain code in the long term, helping you become a better, more thoughtful developer.

. . . . . . . . .