SOLID Principles
Have you ever encountered a class that seems to do everything? It fetches data from the database, performs complex business calculations, formats that data into HTML or JSON, and maybe even logs errors to a file. This all-in-one class, often called a "God Class," is a common anti-pattern.
Consider this Report class:
public class Report {
private String content;
public void generateReportData() {
// Business logic to calculate and gather data
this.content = "Calculated report data...";
System.out.println("Data generation complete.");
}
public void saveToFile(String filePath) {
// Persistence logic to write the report to a file
System.out.println("Saving report to " + filePath);
// ... file I/O operations
}
public void sendByEmail(String recipient) {
// Networking and formatting logic to send an email
System.out.println("Emailing report to " + recipient);
// ... SMTP connection, email formatting, etc.
}
}
This class seems convenient at first. But what happens when a requirement changes?
This class is brittle, hard to test, and difficult to understand because it has too many responsibilities.
The Single Responsibility Principle, as defined by Robert C. Martin, states:
A class should have only one reason to change.
"Doing only one thing" is too vague. A better way to frame it is to think about actors or stakeholders.
Since these three actors would request changes for different reasons, their concerns should live in different classes. A class should be responsible to a single actor.
Let's refactor our Report class by separating its responsibilities into distinct classes, each with a clear purpose.
Step 1: The Data-focused Class This class is only responsible for the core data and the business logic to generate it.
public class Report {
private String content;
public void generateReportData() {
// Business logic to calculate and gather data
this.content = "Calculated report data...";
System.out.println("Data generation complete.");
}
public String getContent() {
return content;
}
}
Step 2: The Persistence-focused Class This class's only job is to handle how a report is stored.
public class ReportPersistence {
public void saveToFile(Report report, String filePath) {
// Persistence logic to write the report to a file
System.out.println("Saving report to " + filePath);
// ... file I/O operations using report.getContent()
}
}
Step 3: The Delivery-focused Class This class is only concerned with sending the report.
public class ReportEmailer {
public void sendByEmail(Report report, String recipient) {
// Networking and formatting logic to send an email
System.out.println("Emailing report to " + recipient);
// ... SMTP connection, email formatting using report.getContent(), etc.
}
}
Now, a change to the email logic only affects ReportEmailer. The core Report class remains untouched and stable. Each class is easy to understand, test, and maintain.
A key discussion point in an interview is the granularity of SRP. If taken to an extreme, you could end up with a "class explosion," where every single method gets its own class. This is not the goal.
ReportPersistence class might have methods for saveToFile, loadFromFile, and deleteFile. These are multiple methods, but they all serve the single responsibility of persistence.The skill is in defining the responsibility at the correct level. A good rule of thumb is to ask, "If I change this, what is the blast radius? Am I changing code that serves a completely different business need?" If the answer is yes, you are likely violating SRP.
Report's calculation logic with a simple unit test. You can test ReportPersistence by mocking the Report object.OrderService, PaymentService, NotificationService) and has only one reason to change: a change in that specific business capability.