Separation of Concerns

The development process has too many different responsibilities. User interface design, database processes, exception handling, and many other tasks can be examples of responsibilities. Separation Of Concerns recommends that these different responsibilities be isolated from each other and each handled within a separate component. This makes the software more sustainable and maintainable.

As the simplest definition; we can define it as a component focusing only on its own work. For example; All structures such as logic, business, and helpers are located in different places and managed from here. The simplest example can be said to be MVC. MVC is a structure derived from Separation Of Concerns, and considering Model-View-Controller from different points comes from this principle. Of course, this can be described as the simplest form of Separation Of Concerns. The main purpose of this principle is that we should divide all requirements into the smallest parts that can be divided.

Why Separation Of Concern?

Separation Of Concerns is one of the most fundamental principles in software development. In fact, it is so critical that two of the 5 SOLID principles (Single Responsibility Principle and Interface Segregation Principle) are directly derived from this concept.

The active implementation of Separation Of Concerns will prevent the disruption or impact of other structures when adding or enhancing new modules or services within our application. This ensures that the task at hand is completed quickly, more efficiently, and securely by focusing on a single point.

In addition to all these benefits, it directly influences high code quality because the interdependence between codes is kept at the lowest level, making the code easier to understand.

As mentioned earlier, Separation Of Concerns is just the foundation of this task. When dealing with architecture and aiming to find a solution, a variety of technical options and technologies come into play. For example, various options such as DDD (Domain Driven Design), TDD (Test Driven Development), MVC, AOP (Aspect Oriented Programming) are available. However, fundamentally, all structures somehow either become Separation Of Concerns or support it.

Advantages In Software Design:

Modularity: Breaking down the software into small components enables us to design and develop each one separately. This ensures that when changes affect one component, the impact on other components is minimized.

Usability: Separating each component provides a better experience for users. Users can focus only on the components they need, avoiding unnecessary complexity.

Conceptual Integrity: Addressing each component separately improves conceptual integrity. This allows us to better understand what each component does and how they are interconnected within the system.

Successful SoC Example: In this example, we will examine C# code handling a shopping cart. As a successful example of SoC, I present a sample that keeps the code clean and well-separated.

public class ShoppingCart
{
    private List<Item> items = new List<Item>();

    public void AddItem(Item item)
    {
        items.Add(item);
    }

    public void RemoveItem(Item item)
    {
        items.Remove(item);
    }

    public decimal CalculateTotal()
    {
        decimal total = 0;
        foreach (var item in items)
        {
            total += item.Price;
        }
        return total;
    }
}

public class Item
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

In this example, the ShoppingCart and Item classes are separated. While ShoppingCart manages the functionality of the shopping cart, Item represents shopping items. Each class focuses on its own responsibility and is organized in accordance with the SoC principle.

Unsuccessful SoC Example: Below, an unsuccessful example of SoC is presented. This example includes complex C# code that combines user authentication and shopping cart operations:

public class User
{
    public string Username { get; set; }
    public string Password { get; set; }
    private List<Item> shoppingCart = new List<Item>();

    public void AddItemToCart(Item item)
    {
        shoppingCart.Add(item);
    }

    public void RemoveItemFromCart(Item item)
    {
        shoppingCart.Remove(item);
    }

    public bool Authenticate(string username, string password)
    {
        if (Username == username && Password == password)
        {
            return true;
        }
        else
        {
            return false;
        }
    }

    public decimal CalculateTotal()
    {
        decimal total = 0;
        foreach (var item in shoppingCart)
        {
            total += item.Price;
        }
        return total;
    }
}

public class Item
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

In this example, the User class combines user authentication, shopping cart operations, and managing the shopping cart functionality in the same class. This leads to complexity in the code, violates the SoC principle, and sets the stage for the God Class problem.

A successful SoC example reflects a scenario where each class focuses on a specific responsibility, and each class has its own unique functionality. In contrast, an unsuccessful SoC example combines different responsibilities, making the code complex. The SoC principle helps the code be cleaner, more sustainable, and less prone to errors.

Successful SoC Example 2: As another example, let’s consider a SoC-compliant part of a customer order management application

public class CustomerOrder
{
    public int OrderId { get; set; }
    public List<Product> Products { get; set; }
    public decimal TotalPrice { get; set; }
}

public class OrderProcessor
{
    public CustomerOrder CreateOrder(List<Product> selectedProducts)
    {
        // Create and return a new order.
    }

    public void CalculateTotalPrice(CustomerOrder order)
    {
        // Calculate the total price of the order.
    }
}

In this example, the CustomerOrder class represents a customer order, while the OrderProcessor performs order creation and price calculation functions. Each class has a clear and separated role.

Unsuccessful SoC Example 2: In the following example, you will find an unsuccessful version of a customer order management application that does not adhere to SoC.

public class CustomerOrder
{
    public int OrderId { get; set; }
    public List<Product> Products { get; set; }
    public decimal TotalPrice { get; set; }

    public void CreateOrder(List<Product> selectedProducts)
    {
        // Create a new order.
    }

    public void CalculateTotalPrice()
    {
        // Calculate the total price of the order.
    }

    public void SaveOrderToDatabase()
    {
        // Save order to database.
    }
}

In this example, the CustomerOrder class includes both order creation and price calculation, as well as saving to the database. This brings together different concerns and does not adhere to the SoC principle.

These examples can be used to illustrate why SoC is important and how it should be implemented. Successful SoC helps make the code more maintainable, understandable, and extensible.

Successful SoC and DDD Example: The following example demonstrates the successful use of SoC and DDD in an e-commerce application

public class Product
{
    public int ProductId { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    // Other product features and business logic.
}

public class Order
{
    public int OrderId { get; set; }
    public List<OrderLine> OrderLines { get; set; }
    public DateTime OrderDate { get; set; }
    // Other order properties and business logic.

    public void AddProductToOrder(Product product, int quantity)
    {
        // Processes adding products to the order.
    }

    public decimal CalculateTotalPrice()
    {
        // Calculates the total price of the order.
    }
}

public class OrderLine
{
    public int OrderLineId { get; set; }
    public Product Product { get; set; }
    public int Quantity { get; set; }
    // Other order line features.
}

// Application Services Layer.
public class OrderService
{
    private IRepository<Order> orderRepository;

    public OrderService(IRepository<Order> orderRepository)
    {
        this.orderRepository = orderRepository;
    }

    public void PlaceOrder(Order order)
    {
        // Saves a new order.
        orderRepository.Save(order);
    }
}

In this example, the Product, Order, and OrderLine classes represent the core business logic in the domain layer. The OrderService manages order-related processes in the application services layer. This design cleanly separates business logic and database access, adhering to DDD principles.

Unsuccessful SoC and DDD Example: The following example represents an e-commerce application that does not adhere to DDD and SoC.

public class ECommerceApp
{
    public void PlaceOrder(int productId, int quantity)
    {
        // Contains business logic such as recording the order and calculating the total price.
    }

    public Product GetProductInfo(int productId)
    {
        // Gets product information from database.
    }
}

In this example, the ECommerceApp class includes both order creation and obtaining product information. This violates the SoC principle by combining different responsibilities and does not adhere to DDD principles by not using a clean domain model.

A successful SoC and DDD application defines business logic clearly, separates the domain model from application services, and makes large and complex projects more manageable.

Last updated