Separation of Concerns in C#: Best Practices and Code Examples


Separation of concerns (SoC) is a fundamental software design principle aimed at dividing a program into distinct sections, each responsible for a specific functionality. The goal is to reduce complexity by organizing code in such a way that each section or "concern" only handles one aspect of the application. This leads to easier maintenance, greater flexibility, and improved readability of the code. SoC makes it easier to locate and modify parts of a program when specific changes are needed..


In C#, the SoC principle can be implemented through various programming constructs like classes, methods, and interfaces. A classic example of this is the Model-View-Controller (MVC) pattern, commonly used in web applications. In this architecture, the "Model" handles data and business logic, the "View" manages the user interface, and the "Controller" acts as an intermediary between the model and the view. By separating these concerns, changes to the UI (View) won't affect the business logic (Model) or how data is controlled (Controller).


Let's have a look at an example of an order processing service:

public class OrderService
{
    public void ProcessOrder(Order order)
    {
        // Validate the order
        if (order.Items.Count == 0)
        {
            throw new Exception("Order must have at least one item.");
        }

        // Calculate tax
        decimal tax = order.TotalAmount * 0.08m;
        order.TotalAmount += tax;

        // Send order confirmation email
        string customerEmail = order.CustomerEmail;
        string message = $"Dear {order.CustomerName}, your order has been processed. Total: {order.TotalAmount}";
        SendEmail(customerEmail, message);

        // Log the order processing
        Console.WriteLine($"Order for {order.CustomerName} processed with total {order.TotalAmount}");
    }

    private void SendEmail(string email, string message)
    {
        // Simulated email sending logic
        Console.WriteLine($"Email sent to {email}: {message}");
    }
}


In this example, the ProcessOrder method is doing too many things: It validates the order. It calculates taxes. It sends an email. It logs the result. Let's see how this code code could be refactored:

public class OrderService
{
    public void ProcessOrder(Order order)
    {
        ValidateOrder(order);              // Validate the order
        CalculateTax(order);               // Calculate and add tax
        SendOrderConfirmationEmail(order); // Send confirmation email
        LogOrderProcessing(order);         // Log the order processing
    }

    private void ValidateOrder(Order order)
    {
        if (order.Items.Count == 0)
        {
            throw new Exception("Order must have at least one item.");
        }
    }

    private void CalculateTax(Order order)
    {
        decimal tax = order.TotalAmount * 0.08m; // Assume 8% tax
        order.TotalAmount += tax;
    }

    private void SendOrderConfirmationEmail(Order order)
    {
        string customerEmail = order.CustomerEmail;
        string message = $"Dear {order.CustomerName}, your order has been processed. Total: {order.TotalAmount}";
        SendEmail(customerEmail, message);
    }

    private void SendEmail(string email, string message)
    {
        // Simulated email sending logic
        Console.WriteLine($"Email sent to {email}: {message}");
    }

    private void LogOrderProcessing(Order order)
    {
        Console.WriteLine($"Order for {order.CustomerName} processed with total {order.TotalAmount}");
    }
}

Key Points to Consider


Grouping Methods by Concern: If you find multiple methods that are related to a particular aspect of your system, such as validation, tax calculation, or logging, it might be a sign that they should be grouped together. When this happens, creating a dedicated class makes sense. This not only makes the code cleaner but also aligns with the **single responsibility principle** (each class should have only one reason to change).


The Rule-of-Thumb: The idea that if you have more than two methods related to the same concern, you should consider creating a separate class, is a useful heuristic. It helps in keeping your classes small and focused, leading to better modularity. For example, if you have methods like ValidateOrder and ValidateCustomer, it makes sense to extract them into a ValidationService class.


Not Always Clear-Cut: Deciding when to split methods into separate classes isn't always straightforward. You need to balance concerns like simplicity, reusability, and future scalability. Over-abstracting too early can lead to unnecessary complexity, while under-abstracting can make the code harder to manage as the application grows.

Log in to mark this article as read and save your progress.
An unhandled error has occurred. Reload 🗙