Single Responsibility principle – Explanation and the real-world example
Let’s first explain a single responsibility principle on the class example and later we will show how it applies in other areas as well! On the class level, SRP
says that one class should have one responsibility.
Let’s say you have to create a program that goes to your favorite website for concerts and write information about those concerts into an Excel file. You would like to collect information only about future concerts. If the concert is in the past, you would not like to scrap that concert.
I used jsoup
and apache poi
for scraping from the website and writing into an Excel file but that part is irrelevant.
Now, this is our first try:
public class Scraper {
public void scrapConcerts() throws IOException {
List<String> urlList = new ArrayList<>();
urlList.add("https://somewebsite.com/concert1");
urlList.add("https://somewebsite.com/concert2");
urlList.add("https://somewebsite.com/concert3");
urlList.add("https://somewebsite.com/concert4");
for ( int i = 0; i < urlList.size(); i++) {
//connect to the url
Document document = Jsoup.connect(urlList.get(i)).get();
//scrap concert date
LocalDateTime concertDate = LocalDateTime.parse(document.select(".description [itemProp=startDate]").attr("content"), DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX"));
//validate date -> if date is in the past, we should skip that concert
if (concertDate.isBefore(LocalDateTime.now())) {
continue;
}
//at the end, write that concert into our excel file
XSSFWorkbook workbook = new XSSFWorkbook();
XSSFSheet sheet = workbook.createSheet("Concerts");
sheet.createRow(i).createCell(1).setCellValue(concertDate);
try (FileOutputStream outputStream = new FileOutputStream("Concerts.xlsx")) {
workbook.write(outputStream);
}
}
}
}
Here, we broke the Single Responsibility principle and this is a very common mistake. Our scraper class has three responsibilities:
- connect to the website, parse HTML, and extract concert date somehow
- validate data(see if concert is in the past)
- write data into an Excel file
If we write a unit test for this class and the test fails, we don’t know what is wrong. Is it scraping, validation, or writing into Excel? Also, if we want to change validation, we will change this class but we cannot be sure that by changing validation we did not break writing into files.
Divide our code into three parts
Now, let’s split this into three classes:
public class Concert {
private UUID id;
private String name;
private LocalDateTime date;
public Concert(UUID id, String name, LocalDateTime date) {
this.id = id;
this.name = name;
this.date = date;
}
//getters and setters
}
The responsibility of the Concert
class is to keep data about concerts.
public interface Validator {
boolean validate(Concert concert);
}
public class ValidatorImpl implements Validator {
//As you can see, this line can be simplified even more but I didn't want to confuse beginners with one liners :)
@Override
public boolean validate(Concert concert) {
if (concert.getDate().isBefore(LocalDateTime.now())) {
return false;
}
return true;
}
}
The responsibility of the Validator
is only to validate the concert and tell us if the concert is valid or not. As you can see we program to an interface so we can easily substitute our implementation later.
public interface Writer {
void write(List<Concert> concerts) throws IOException;
}
public class ExcelWriter implements Writer {
@Override
public void write(List<Concert> concerts) throws IOException {
XSSFWorkbook workbook = new XSSFWorkbook();
XSSFSheet sheet = workbook.createSheet("Concerts");
for ( int i = 0; i < concerts.size(); i++) {
sheet.createRow(i).createCell(1).setCellValue(concerts.get(i).getDate());
try (FileOutputStream outputStream = new FileOutputStream("Concerts.xlsx")) {
workbook.write(outputStream);
}
}
}
}
The responsibility of the Writer
is to write our concerts somewhere. Our Excel implementation writes our concerts to an Excel file.
public interface HtmlParser {
List<Concert> parse(List<String> urlList) throws IOException;
}
public class JsoupHtmlParser implements HtmlParser {
@Override
public List<Concert> scrap(List<String> urlList) throws IOException {
List<Concert> concerts = new ArrayList<>();
for ( int i = 0; i < urlList.size(); i++) {
Document document = Jsoup.connect(urlList.get(i)).get();
LocalDateTime concertDate = LocalDateTime.parse(document.select(".description [itemProp=startDate]").attr("content"), DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX"));
//I wrote some hardcoded values for id and name since those are not relevant for our example
concerts.add(new Concert(UUID.randomUUID(), "Concert name", concertDate));
}
return concerts;
}
}
Finally, the responsibility of HtmlParser is just to parse concerts(it doesn’t care about validation or writing into the file)
Now our program looks much better:
public class Scraper {
private final Validator validator;
private final Writer writer;
private final HtmlParser parser;
public Scraper() {
this.validator = new ValidatorImpl();
this.writer = new ExcelWriter();
this.parser = new JsoupHtmlParser();
}
public void scrapConcerts() throws IOException {
List<String> urlList = new ArrayList<>();
urlList.add("https://somewebsite.com/concert1");
urlList.add("https://somewebsite.com/concert2");
urlList.add("https://somewebsite.com/concert3");
urlList.add("https://somewebsite.com/concert4");
for ( int i = 0; i < urlList.size(); i++) {
List<Concert> concerts = parser.parse(urlList);
List<Concert> validConcerts = concerts
.stream()
.filter(validator::validate)
.collect(Collectors.toList());
writer.write(validConcerts);
}
}
}
If we want to test if validation is working we can test the Validator
class and be sure that we will not break anything else. If we want to change the way how data is written into the Excel file we have to change ExcelWriter
class and we can be sure that everything else is working properly.
We resolved the problem but we have a new one. Can you spot it? We violate Dependency injection principle. You can read more about that principle in [this] article.
Other examples of SRP principle violations
I told you at the beginning of the article that SRP can be violated on other levels(not only on the class level). Let me mention two more examples:
- Software architecture level. You usually divide complex applications into separate services. Every service has one responsibility. If you look at some
Domain Driven Design
books you will find advice that one service should be the size of onebounded context
. There are two reasons for that. The first one is that we want to decompose a system into a logical isolated services so we can maintain those services independently. Even more important reason is the ability to scale the project to multiple teams. Take a note of how this “scaling project to multiple teams” advice is not directly a technical thing but more of an organizational thing. It makes no sense to create microservices that are small so that one person creates and maintains ten microservices. The ideal scenario is to create a microservice that can be developed and maintained by one to five people so that you can assign one team to one microservice. - When you commit your changes to the
GIT
repository, you should applySRP
. Onecommit
should have one responsibility. If you make a commit and you changed 50 files, you violatedSRP
and your change has more than one responsibility.
Further reading
If you like to learn more about design patterns or best practices [here](https://javamentor.net/category/patterns-and-best-practices/) you can find more posts that I wrote on that topic.