Understanding the Nuances of the Single Responsibility Principle
Written on
Chapter 1: Introduction to Single Responsibility Principle
The Single Responsibility Principle (SRP), as articulated by Robert C. Martin, is a foundational concept in programming. It posits that:
- A module should serve a single purpose for one actor.
- A class must have only one reason to undergo modification.
In this discussion, we will examine the intricacies of SRP and how its application can be more complex than it initially appears.
Scenario Exploration
Let’s consider a scenario where we need to develop a class responsible for calculating a company's profit using the straightforward formula:
profit = earnings — expenses
For the sake of this discussion on architecture, we will not delve into the specifics of how earnings and expenses are computed.
Single Interface Approach
At first glance, one might think that profit calculation is a singular process. Thus, the implementation could be as follows:
interface ProfitCalculator {
BigDecimal calculate(Company company);
}
class ProfitCalculatorImpl implements ProfitCalculator {
@Override
public BigDecimal calculate(Company company){
BigDecimal earnings = calculateEarnings(company);
BigDecimal expenses = calculateExpenses(company);
return earnings.subtract(expenses);
}
private BigDecimal calculateEarnings(Company company){
// Logic to compute earnings}
private BigDecimal calculateExpenses(Company company){
// Logic to compute expenses}
}
// Usage
ProfitCalculator profitCalculator = new ProfitCalculatorImpl();
BigDecimal profit = profitCalculator.calculate(company);
Advantages of this Solution:
- A single interface simplifies the profit calculation process.
- The user only needs to invoke it with the company as a parameter.
- The entire profit calculation logic resides within this class, making it quick to implement.
Disadvantages:
- The extensive logic for calculating earnings and expenses can lead to a bloated class.
- Modifying how earnings are calculated for specific companies can become cumbersome.
- Unit testing may be challenging due to the potential for numerous branches in the class.
This method is acceptable when one can reasonably assume that the calculations for earnings and expenses will remain straightforward and stable over time. While such information might be easily obtainable in a short-term project, it is often uncertain in long-term endeavors.
Multiple Interface Strategy
An alternative approach involves dividing the profit interface into two separate components: EarningsInterface and ExpensesInterface.
interface ProfitCalculator {
BigDecimal calculate(Company company);
}
interface EarningsCalculator {
BigDecimal calculate(Company company);
}
interface ExpensesCalculator {
BigDecimal calculate(Company company);
}
class ProfitCalculatorImpl implements ProfitCalculator {
private EarningsCalculator earningsCalculator;
private ExpensesCalculator expensesCalculator;
public ProfitCalculatorImpl(
EarningsCalculator earningsCalculator,
ExpensesCalculator expensesCalculator){
this.earningsCalculator = earningsCalculator;
this.expensesCalculator = expensesCalculator;
}
@Override
public BigDecimal calculate(Company company){
BigDecimal earnings = earningsCalculator.calculate(company);
BigDecimal expenses = expensesCalculator.calculate(company);
return earnings.subtract(expenses);
}
}
class EarningsCalculatorImpl implements EarningsCalculator {
@Override
public BigDecimal calculate(Company company){
// Logic to compute earnings}
}
class ExpensesCalculatorImpl implements ExpensesCalculator {
@Override
public BigDecimal calculate(Company company){
// Logic to compute expenses}
}
// Usage
ProfitCalculator profitCalculator =
new ProfitCalculatorImpl(
new EarningsCalculatorImpl(),
new ExpensesCalculatorImpl());
BigDecimal profit = profitCalculator.calculate(company);
Advantages of this Approach:
- Smaller classes enhance maintainability and comprehension.
- Modifying the calculation methods for earnings and expenses is more straightforward.
- Unit testing becomes easier due to reduced class size and complexity.
Disadvantages:
- The object construction process is more intricate, particularly if frameworks like Spring are not utilized for dependency injection.
- The introduction of three interfaces and classes may seem excessive for some scenarios, potentially complicating maintenance.
- Implementing this solution can be more time-consuming.
In conclusion, while the Single Responsibility Principle may seem simple at first, a deeper examination reveals various architectural choices that may require more information than initially apparent.
We can always adhere to the KISS (Keep It Simple, Stupid) principle, beginning with the most straightforward code possible. As new requirements arise, small refactoring can lead to continuous improvements. This method allows for the timely delivery of the initial code version, while each enhancement further refines the overall codebase.
Chapter 2: Learning Through Visual Content
To enhance your understanding of the Single Responsibility Principle, we recommend watching the following videos:
This video titled "SOLID - Single Responsibility Principle [SRP] with Java" provides a concise overview of SRP, illustrating its application in Java programming.
In "No BS SOLID Principles: Single Responsibility Principle," the video delves deeper into SRP, offering practical insights and examples.