SOLID Principles in Java: From Theory to Advanced Practice
The SOLID principles are the foundation of object-oriented design, helping developers create code that is resilient to change, easily testable, and scalable. For a Java developer, understanding these principles at an advanced level is not just about knowing definitions, but about being able to see their violations in legacy code and applying patterns for refactoring. In this article, we will analyze each principle with a focus on complex but typical situations.
1. Single Responsibility Principle (SRP): The Line Between Responsibility and Cohesion
Theory: A class should have only one reason to change. In practice, this means that a class should be responsible for one part of the program's functionality.
Advanced Practice: Often, SRP violation manifests not in an explicit mixing of logic, but in a "blurred" responsibility. Consider an example of a class for working with reports:
// SRP violation: the class handles data, formatting, and sendingpublic class ReportService { public void generateReport(List<Data> data) { // Calculation logic Map<String, Integer> aggregated = aggregate(data); // Formatting (HTML) String html = formatToHtml(aggregated); // Sending via email sendEmail(html, "admin@example.com"); }
private Map<String, Integer> aggregate(List<Data> data) { // ... return new HashMap<>(); }
private String formatToHtml(Map<String, Integer> data) { // ... return "<html>...</html>"; }
private void sendEmail(String content, String to) { // ... }}Problem: if the report format changes (PDF needed instead of HTML), this class must be modified. If the sending method changes (Telegram instead of email) — again. The solution is to split into three classes:
public class ReportAggregator { public Map<String, Integer> aggregate(List<Data> data) { ... }}
public class HtmlReportFormatter { public String format(Map<String, Integer> data) { ... }}
public class EmailSender { public void send(String content, String to) { ... }}
// Composition in a servicepublic class ReportGenerationFacade { private ReportAggregator aggregator; private HtmlReportFormatter formatter; private EmailSender sender;
public void generateAndSend(List<Data> data) { var aggregated = aggregator.aggregate(data); var html = formatter.format(aggregated); sender.send(html, "admin@example.com"); }}2. Open-Closed Principle (OCP): The Art of Extension Through Abstractions
Theory: Classes should be open for extension but closed for modification.
Advanced Practice: A typical violation is chains of if-else or switch statements for different types of objects. An advanced approach is using the Strategy or Template Method pattern with polymorphism.
// OCP violation: adding a new payment type requires modifying the classpublic class PaymentProcessor { public void processPayment(String type, double amount) { if ("CREDIT_CARD".equals(type)) { // logic for credit card } else if ("PAYPAL".equals(type)) { // logic for PayPal } else if ("BITCOIN".equals(type)) { // logic for Bitcoin } }}Fix using an interface and a factory:
public interface PaymentMethod { void pay(double amount);}
public class CreditCardPayment implements PaymentMethod { public void pay(double amount) { ... }}
public class PayPalPayment implements PaymentMethod