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:
Single Responsibility Principle: Each Use Case should have one responsibility, which makes it easier to understand, maintain, and test.
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.
Dependency Injection: Use dependency injection to decouple your Use Cases from specific implementations of services or repositories. This will help improve testability and maintainability.
Error Handling: Handle errors gracefully within your Use Cases to ensure that your application can respond to unexpected situations without crashing.
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}");
}
}
Breakdown of the Code
Dependency Injection: The
ProcessPayrollUseCase
constructor takes anIEmployeeRepository
as a parameter. This repository interface abstracts away the data access layer, allowing for easier testing and flexibility in implementation.-
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.
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);
}
}
Breakdown of the Tests
Setup Method: This method initializes a mock repository and the
ProcessPayrollUseCase
before each test.CalculateNetSalary_ValidEmployee_ReturnsCorrectNetSalary: This test verifies that the
CalculateNetSalary
method returns the expected net salary for a valid employee.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)