Single Responsibility Principle
The Single Responsibility Principle (SRP) is a general software development guideline to follow that contributes to writing code that is easier to manage and maintain.
You can think about SRP in two complementary ways:
The SRP is usually applied to classes and methods. SRP is concerned with one particular behavior, concept, or category.
It leads to code that is more robust because there is one specific reason why it should change rather than multiple concerns.
The reason why multiple concerns is problematic is, as you saw earlier, it complicates code maintainability by potentially introducing bugs in several places. It can also make the code harder to understand and change.
So how do you apply SRP in the code shown in Example 2-2? It is clear that the main class has multiple responsibilities that can be broken down individually:
-
Reading input
-
Parsing the input in a given format
-
Processing the result
-
Reporting a summary of the result
We will focus on the parsing part in this chapter. You will learn how to extend the Bank Statements Analyzer in the next chapter so that it is completely modularized.
The first natural step is to extract the CSV parsing logic into a separate class so you can reuse it for different processing queries.
Let’s call it BankStatementCSVParser
so it is immediately clear what it does (Example 2-3).
You can see that the class BankStatementCSVParser
declares two methods, parseFromCSV()
and parseLinesFromCSV()
, that generate BankTransaction
objects, which is a domain class that models a bank statement (see Example 2-4 for its declaration).
Note
What does domain mean? It means the use of words and terminology that match the business problem (i.e., the domain at hand).
The BankTransaction
class is useful so that different parts of our application share the same common understanding of what a bank statement is. You will notice that the class provides implementation for the methods equals
and hashcode
. The purpose of these methods and how to implement them correctly is covered in Chapter 6.
Example 2-4. A domain class for a bank transaction
public
class
BankTransaction
{
private
final
LocalDate
date
;
private
final
double
amount
;
private
final
String
description
;
public
BankTransaction
(
final
LocalDate
date
,
final
double
amount
,
final
String
description
)
{
this
.
date
=
date
;
this
.
amount
=
amount
;
this
.
description
=
description
;
}
public
LocalDate
getDate
()
{
return
date
;
}
public
double
getAmount
()
{
return
amount
;
}
public
String
getDescription
()
{
return
description
;
}
@Override
public
String
toString
()
{
return
"BankTransaction{"
+
"date="
+
date
+
", amount="
+
amount
+
", description='"
+
description
+
'''
+
'}'
;
}
@Override
public
boolean
equals
(
Object
o
)
{
if
(
this
==
o
)
return
true
;
if
(
o
==
null
||
getClass
()
!=
o
.
getClass
())
return
false
;
BankTransaction
that
=
(
BankTransaction
)
o
;
return
Double
.
compare
(
that
.
amount
,
amount
)
==
0
&&
date
.
equals
(
that
.
date
)
&&
description
.
equals
(
that
.
description
);
}
@Override
public
int
hashCode
()
{
return
Objects
.
hash
(
date
,
amount
,
description
);
}
}
Now you can refactor the application so that it uses your BankStatementCSVParser
, in particular its parseLinesFromCSV()
method, as shown in Example 2-5.
Example 2-5. Using the bank statement CSV parser
final
BankStatementCSVParser
bankStatementParser
=
new
BankTransactionCSVParser
();
final
String
fileName
=
args
[
0
];
final
Path
path
=
Paths
.
get
(
RESOURCES
+
fileName
);
final
List
<
String
>
lines
=
Files
.
readAllLines
(
path
);
final
List
<
BankTransaction
>
bankTransactions
=
bankStatementParser
.
parseLinesFromCSV
(
lines
);
System
.
out
.
println
(
"The total for all transactions is "
+
calculateTotalAmount
(
bankTransactions
));
System
.
out
.
println
(
"Transactions in January "
+
selectInMonth
(
BankTransactions
,
Month
.
JANUARY
));
The different queries you have to implement no longer need to know about internal parsing details, as you can now use BankTransaction
objects directly to extract the information required. The code in Example 2-6 shows how to declare the methods calculateTotalAmount()
and selectInMonth()
, which are responsible for processing the list of transactions and returning an appropriate result. In Chapter 3 you will get an overview of lambda expressions and the Streams API, which will help tidy the code further.
Example 2-6. Processing lists of bank transactions
public
static
double
calculateTotalAmount
(
final
List
<
BankTransaction
>
bankTransactions
)
{
double
total
=
0
d
;
for
(
final
BankTransaction
bankTransaction:
bankTransactions
)
{
total
+=
bankTransaction
.
getAmount
();
}
return
total
;
}
public
static
List
<
BankTransaction
>
selectInMonth
(
final
List
<
BankTransaction
>
bankTransactions
,
final
Month
month
)
{
final
List
<
BankTransaction
>
bankTransactionsInMonth
=
new
ArrayList
<>();
for
(
final
BankTransaction
bankTransaction:
bankTransactions
)
{
if
(
bankTransaction
.
getDate
().
getMonth
()
==
month
)
{
bankTransactionsInMonth
.
add
(
bankTransaction
);
}
}
return
bankTransactionsInMonth
;
}
The key benefit with this refactoring is that your main application is no longer responsible for the implementation of the parsing logic. It is now delegating that responsibility to a separate class and methods that can be maintained and updated independently. As new requirements come in for different queries, you can reuse the functionality encapsulated by the BankStatementCSVParser
class.
In addition, if you need to change the way the parsing algorithm works (e.g., a more efficient implementation that caches results), you now have just a single place that needs to change. Moreover, you introduced a class called BankTransaction
that other parts of your code can rely on without depending on a specific data format pattern.
It is a good habit to follow the principle of least surprise when you implement methods. It will help ensure that it is obvious what is happening when looking at the code. This means:
-
Use self-documenting method names so it is immediately obvious what they do (e.g., calculateTotalAmount()
)
-
Do not change the state of parameters as other parts of code may depend on it
The principle of least surprise can be a subjective concept, though. When in doubt, speak to your colleagues and team members to ensure everyone is aligned.