DEV Community

Cover image for 3 - Clean Architecture: Understanding Use Cases
Daniel Azevedo
Daniel Azevedo

Posted on

3 - Clean Architecture: Understanding Use Cases

Welcome back to our series on Clean Architecture! In the previous posts, we established the foundational concepts of Clean Architecture and delved into Entities. We created our Employee entity and learned how to keep our business logic clean and maintainable. Today, we’ll turn our attention to Use Cases—the driving force of our application’s behavior.

What are Use Cases?

In the context of Clean Architecture, Use Cases represent the application-specific business rules. They define what the application does and how it interacts with the core business logic encapsulated in the Entities. Use Cases orchestrate the flow of data between the user interface, the entities, and any external systems, ensuring that the application behaves as expected.

A Use Case should:

  • Describe a specific business process or functionality.
  • Contain the logic necessary to fulfill that business process.
  • Interact with one or more entities to execute the required behavior.

Best Practices for Use Cases

To ensure your Use Cases adhere to best practices, consider the following guidelines:

  1. Single Responsibility Principle: Each Use Case should have one responsibility, which makes it easier to understand, maintain, and test.

  2. Keep it Simple: Avoid adding unnecessary complexity to your Use Cases. They should focus on orchestrating the flow of data and executing the business logic defined in the entities.

  3. Dependency Injection: Use dependency injection to decouple your Use Cases from specific implementations of services or repositories. This will help improve testability and maintainability.

  4. Error Handling: Handle errors gracefully within your Use Cases to ensure that your application can respond to unexpected situations without crashing.

  5. Encapsulate Business Rules: Business rules should be encapsulated within the Use Cases, leveraging the entities to enforce invariants.

Implementing a Payroll Processing Use Case

Now, let’s implement a Process Payroll Use Case for our payroll system. This Use Case will be responsible for calculating the net salary of an employee and possibly triggering additional business processes like saving the results to a database or notifying relevant parties.

Here’s how we might implement the ProcessPayrollUseCase:

public class ProcessPayrollUseCase
{
    private readonly IEmployeeRepository _employeeRepository;

    public ProcessPayrollUseCase(IEmployeeRepository employeeRepository)
    {
        _employeeRepository = employeeRepository;
    }

    public decimal CalculateNetSalary(int employeeId)
    {
        var employee = _employeeRepository.GetEmployeeById(employeeId);
        if (employee == null)
        {
            throw new ArgumentException("Employee not found.");
        }

        employee.ValidateTaxRate();
        decimal totalBeforeTax = employee.CalculateTotalBeforeTax();
        return totalBeforeTax - (totalBeforeTax * employee.TaxRate);
    }

    public void ProcessPayroll(int employeeId)
    {
        var netSalary = CalculateNetSalary(employeeId);
        // Here you could add additional logic like saving to a database or notifying the employee
        Console.WriteLine($"Net salary for employee {employeeId}: {netSalary:C}");
    }
}
Enter fullscreen mode Exit fullscreen mode

Breakdown of the Code

  1. Dependency Injection: The ProcessPayrollUseCase constructor takes an IEmployeeRepository as a parameter. This repository interface abstracts away the data access layer, allowing for easier testing and flexibility in implementation.

  2. CalculateNetSalary Method:

    • Fetches the employee using the repository.
    • Validates the employee's tax rate.
    • Calculates the total salary before tax and returns the net salary.
  3. ProcessPayroll Method: This method serves as the entry point for processing payroll. It calculates the net salary and could include additional steps like saving the result to a database or notifying other systems.

Testing the Use Case

Testing is a crucial aspect of Clean Architecture. By following the principles we’ve discussed, we can write unit tests for our Use Case without being concerned about the underlying data access implementations.

Here’s a simple example of how you might test the ProcessPayrollUseCase:

[TestClass]
public class ProcessPayrollUseCaseTests
{
    private Mock<IEmployeeRepository> _employeeRepositoryMock;
    private ProcessPayrollUseCase _processPayrollUseCase;

    [TestInitialize]
    public void Setup()
    {
        _employeeRepositoryMock = new Mock<IEmployeeRepository>();
        _processPayrollUseCase = new ProcessPayrollUseCase(_employeeRepositoryMock.Object);
    }

    [TestMethod]
    public void CalculateNetSalary_ValidEmployee_ReturnsCorrectNetSalary()
    {
        var employee = new Employee(1, "John Doe", 5000, 0.2m, 500);
        _employeeRepositoryMock.Setup(repo => repo.GetEmployeeById(1)).Returns(employee);

        var netSalary = _processPayrollUseCase.CalculateNetSalary(1);

        Assert.AreEqual(4000, netSalary); // 5000 + 500 - (5000 + 500) * 0.2 = 4000
    }

    [TestMethod]
    [ExpectedException(typeof(ArgumentException))]
    public void CalculateNetSalary_EmployeeNotFound_ThrowsException()
    {
        _employeeRepositoryMock.Setup(repo => repo.GetEmployeeById(1)).Returns((Employee)null);
        _processPayrollUseCase.CalculateNetSalary(1);
    }
}
Enter fullscreen mode Exit fullscreen mode

Breakdown of the Tests

  1. Setup Method: This method initializes a mock repository and the ProcessPayrollUseCase before each test.

  2. CalculateNetSalary_ValidEmployee_ReturnsCorrectNetSalary: This test verifies that the CalculateNetSalary method returns the expected net salary for a valid employee.

  3. CalculateNetSalary_EmployeeNotFound_ThrowsException: This test checks that the method throws an exception when the employee is not found in the repository.

Conclusion

In this post, we explored the concept of Use Cases in Clean Architecture, emphasizing their role in orchestrating application behavior and encapsulating business logic. We implemented a ProcessPayrollUseCase to calculate net salaries and discussed best practices to ensure maintainability and testability.

Next time, we’ll cover the Interface Adapters layer—how to connect the Use Cases with external systems and how to convert data into a suitable format for the application.

Stay tuned for part 4!

Top comments (0)