Google Anlatics

Wednesday, May 1, 2024

Applying the Single Responsibility Principle (SRP)

Applying the Single Responsibility Principle (SRP) in Report Processing

The Single Responsibility Principle (SRP) is a foundational concept in object-oriented programming that advocates for classes to have only one reason to change. This principle promotes modular, readable, and maintainable code by ensuring that each class or module encapsulates only one responsibility or behavior.

Example Scenario

In our scenario, we have a reporting system responsible for processing various report items asynchronously. To uphold the SRP, we aim to refactor our report processing logic into distinct components that each fulfill a specific responsibility, such as data encapsulation, processing orchestration, and logging.

Implementation

1. ReportItem Class

public class ReportItem
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public ReportItemStatus Status { get; set; }
    public string ErrorMessage { get; set; }
}

The ReportItem class represents a report item entity, encapsulating properties like IdNameStatus, and ErrorMessage. This class is solely responsible for managing data related to a report item.

2. ReportProcessor Class

public class ReportProcessor
{
    private readonly IReportService _reportService;
    private readonly ILogger _logger;

    public ReportProcessor(IReportService reportService, ILogger logger)
    {
        _reportService = reportService;
        _logger = logger;
    }

    public async Task ProcessReportItemAsync(Guid itemId)
    {
        var item = await _reportService.GetReportItemAsync(itemId);

        if (item == null || item.Status != ReportItemStatus.Pending)
        {
            _logger.LogWarning($"Report item with ID {itemId} is not available for processing.");
            return;
        }

        try
        {
            _logger.LogInformation($"Processing report item: {item.Name}");

            item.Status = ReportItemStatus.Processing;
            await _reportService.UpdateReportItemAsync(item);

            await SimulateReportProcessingAsync(item);

            item.Status = ReportItemStatus.Completed;
            await _reportService.UpdateReportItemAsync(item);

            _logger.LogInformation($"Report item processed successfully: {item.Name}");
        }
        catch (Exception ex)
        {
            item.Status = ReportItemStatus.Failed;
            item.ErrorMessage = ex.Message;
            await _reportService.UpdateReportItemAsync(item);

            _logger.LogError($"Failed to process report item: {item.Name}. Error: {ex.Message}");
        }
    }

    private async Task SimulateReportProcessingAsync(ReportItem item)
    {
        // Simulate report processing
    }
}

The ReportProcessor class is dedicated to processing report items asynchronously. It utilizes an injected IReportService for data retrieval and updates and an ILogger for logging processing outcomes and errors. This class demonstrates a clear responsibility focused on orchestrating the report processing workflow.

3. Interfaces

public interface IReportService
{
    Task<ReportItem> GetReportItemAsync(Guid itemId);
    Task UpdateReportItemAsync(ReportItem item);
}

public interface ILogger
{
    void LogInformation(string message);
    void LogWarning(string message);
    void LogError(string message);
}

Interfaces like IReportService and ILogger define contracts for interacting with report data and logging actions, respectively. Leveraging interfaces promotes loose coupling, facilitates dependency injection for enhanced testability, and enables flexibility in swapping implementations.

Conclusion

In this example, we've refactored our report processing logic to adhere to the Single Responsibility Principle (SRP). Each class (ReportItemReportProcessor) embodies a distinct responsibility, such as data encapsulation or processing orchestration. Meanwhile, interfaces (IReportServiceILogger) facilitate decoupling and abstraction, fostering maintainable and extensible code.

By applying SRP, we've established a modular and maintainable design where each component is dedicated to a specific aspect of report processing. This design approach enhances code clarity, adaptability to changing requirements, and adherence to best practices in software design and architecture. Ultimately, embracing SRP contributes to a cleaner and more manageable codebase, promoting robustness and scalability in our reporting system.

Sri Lanka .NET 
                Forum Member