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; }
}
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 Id
, Name
, Status
, 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
}
}
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);
}
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 (ReportItem
, ReportProcessor
) embodies a distinct responsibility, such as data encapsulation or processing orchestration. Meanwhile, interfaces (IReportService
, ILogger
) 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.