Введение: почему SOLID важен для Java-разработчика
В мире объектно-ориентированного программирования поддержка и расширение кода со временем становятся всё сложнее. Без чёткой архитектуры даже небольшой проект превращается в "большой ком грязи", где изменение одной функции ломает десять других. Именно здесь на помощь приходят SOLID принципы — пять фундаментальных правил проектирования классов, сформулированных Робертом Мартином (дядюшкой Бобом).
Для Java-разработчика SOLID — это не просто теория, а практический инструмент. Java, как строго типизированный язык с мощной системой интерфейсов и наследования, идеально подходит для реализации этих принципов. В этой статье мы разберём каждый принцип с точки зрения теории и покажем живые примеры кода на Java, которые можно сразу применить в проекте.
1. Single Responsibility Principle (Принцип единственной ответственности)
Теория
SRP гласит: у класса должна быть только одна причина для изменения. Другими словами, класс должен отвечать за одну, чётко определённую функциональность. Если класс делает слишком много, его становится сложно тестировать, изменять и переиспользовать.
Практика на Java
Рассмотрим типичное нарушение SRP — класс, который одновременно управляет данными пользователя и отправляет уведомления:
// ❌ Нарушение SRP
public class User {
private String name;
private String email;
public void saveToDatabase() {
// Сохранение в БД
}
public void sendWelcomeEmail() {
// Отправка email
}
}
Правильное решение — разделить ответственность на два класса:
// ✅ Соблюдение SRP
public class User {
private String name;
private String email;
// Геттеры и сеттеры
}
public class UserRepository {
public void save(User user) {
// Сохранение в БД
}
}
public class EmailService {
public void sendWelcomeEmail(User user) {
// Отправка email
}
}
Вывод: каждый класс теперь имеет одну причину для изменения. User меняется только при изменении структуры данных, UserRepository — при изменении логики БД, EmailService — при изменении шаблонов писем.
2. Open-Closed Principle (Принцип открытости/закрытости)
Теория
OCP утверждает: классы должны быть открыты для расширения, но закрыты для модификации. Это значит, что новую функциональность нужно добавлять через наследование или композицию, не трогая уже протестированный код.
Практика на Java
Представьте, что у вас есть калькулятор скидок для разных типов клиентов:
// ❌ Нарушение OCP
public class DiscountCalculator {
public double calculateDiscount(String customerType, double amount) {
if (customerType.equals("Regular")) {
return amount * 0.1;
} else if (customerType.equals("Premium")) {
return amount * 0.2;
}
return 0;
}
}
При добавлении нового типа клиента (например, "Gold") придётся изменять существующий класс. Решение — использовать полиморфизм:
// ✅ Соблюдение OCP
public interface DiscountStrategy {
double calculate(double amount);
}
public class RegularDiscount implements DiscountStrategy {
@Override
public double calculate(double amount) {
return amount * 0.1;
}
}
public class PremiumDiscount implements DiscountStrategy {
@Override
public double calculate(double amount) {
return amount * 0.2;
}
}
public class DiscountCalculator {
private DiscountStrategy strategy;
public DiscountCalculator(DiscountStrategy strategy) {
this.strategy = strategy;
}
public double calculate(double amount) {
return strategy.calculate(amount);
}
}
Теперь для добавления "Gold" достаточно создать новый класс GoldDiscount — без изменения DiscountCalculator.
3. Liskov Substitution Principle (Принцип подстановки Барбары Лисков)
Теория
LSP формулируется так: объекты подкласса должны быть взаимозаменяемы с объектами базового класса без нарушения корректности программы. Иными словами, наследник не должен ослаблять или изменять контракт родителя.
Практика на Java
Классическое нарушение — квадрат, наследующий от прямоугольника:
// ❌ Нарушение LSP
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int getArea() { return width * height; }
}
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width); // Нарушение: меняет обе стороны
}
@Override
public void setHeight(int height) {
super.setHeight(height);
super.setWidth(height);
}
}
Если клиент ожидает Rectangle и вызывает setWidth(5); setHeight(10), то для квадрата площадь будет 100, а не 50. Правильное решение — использовать общий интерфейс или композицию:
// ✅ Соблюдение LSP
public interface Shape {
int getArea();
}
public class Rectangle implements Shape {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public int getArea() {
return width * height;
}
}
public class Square implements Shape {
private int side;
public Square(int side) {
this.side = side;
}
@Override
public int getArea() {
return side * side;
}
}
4. Interface Segregation Principle (Принцип разделения интерфейсов)
Теория
ISP рекомендует: клиенты не должны зависеть от методов, которые они не используют. Вместо одного "толстого" интерфейса лучше создать несколько специализированных.
Практика на Java
Пример нарушения — интерфейс для многофункционального принтера:
// ❌ Нарушение ISP
public interface MultiFunctionPrinter {
void print(String document);
void scan(String document);
void fax(String document);
}
public class SimplePrinter implements MultiFunctionPrinter {
@Override
public void print(String document) { /* работает */ }
@Override
public void scan(String document) { throw new UnsupportedOperationException(); }
@Override
public void fax(String document) { throw new UnsupportedOperationException(); }
}
Правильное решение — разделить интерфейсы по функциональности:
// ✅ Соблюдение ISP
public interface Printer {
void print(String document);
}
public interface Scanner {
void scan(String document);
}
public interface Fax {
void