SOLID принципы в Java: от теории к чистому коду
Каждый Java-разработчик рано или поздно сталкивается с проблемой «грязного» кода: классы, которые делают всё сразу, жёсткие зависимости и невозможность расширения без поломки существующей логики. Именно здесь на помощь приходят SOLID принципы — пять фундаментальных правил объектно-ориентированного проектирования, сформулированных Робертом Мартином (дядюшкой Бобом). В этой статье мы разберём каждый принцип на практике с примерами на Java, чтобы ваш код стал гибким, тестируемым и лёгким в поддержке.
1. Single Responsibility Principle (SRP) — принцип единственной ответственности
Суть: «У класса должна быть только одна причина для изменения». Иными словами, класс должен отвечать за одну чётко определённую задачу.
Нарушение SRP
Рассмотрим класс OrderService, который одновременно занимается расчётом цены, сохранением в базу и отправкой email:
public class OrderService {
public void processOrder(Order order) {
// 1. Расчёт стоимости
double total = order.getItems().stream()
.mapToDouble(Item::getPrice)
.sum();
// 2. Сохранение в БД
Database.save(order);
// 3. Отправка уведомления
EmailService.send("Заказ оформлен на сумму: " + total);
}
}
Проблема: Изменение логики отправки писем или способа сохранения заставит менять OrderService. Это нарушает SRP.
Рефакторинг по SRP
Разделяем обязанности на три отдельных класса:
public class PriceCalculator {
public double calculateTotal(Order order) {
return order.getItems().stream()
.mapToDouble(Item::getPrice)
.sum();
}
}
public class OrderRepository {
public void save(Order order) {
Database.save(order);
}
}
public class NotificationService {
public void sendConfirmation(Order order) {
EmailService.send("Заказ оформлен на сумму: " + order.getTotal());
}
}
// Клиентский код
public class OrderProcessor {
private PriceCalculator calculator;
private OrderRepository repository;
private NotificationService notifier;
public void processOrder(Order order) {
double total = calculator.calculateTotal(order);
order.setTotal(total);
repository.save(order);
notifier.sendConfirmation(order);
}
}
Результат: Каждый класс имеет одну причину для изменения. Код становится модульным и тестируемым.
2. Open-Closed Principle (OCP) — принцип открытости/закрытости
Суть: «Программные сущности должны быть открыты для расширения, но закрыты для модификации». Новая функциональность добавляется через расширение, а не через правку существующего кода.
Нарушение OCP
Представим систему скидок, где каждый новый тип скидки требует изменения метода applyDiscount:
public class DiscountService {
public double applyDiscount(String type, double price) {
if (type.equals("SUMMER")) {
return price * 0.9;
} else if (type.equals("VIP")) {
return price * 0.8;
}
// Добавление нового типа → изменение класса
return price;
}
}
Решение через полиморфизм
Используем интерфейс и разные реализации:
public interface Discount {
double apply(double price);
}
public class SummerDiscount implements Discount {
@Override
public double apply(double price) {
return price * 0.9;
}
}
public class VipDiscount implements Discount {
@Override
public double apply(double price) {
return price * 0.8;
}
}
// Новый тип — просто новый класс
public class BlackFridayDiscount implements Discount {
@Override
public double apply(double price) {
return price * 0.5;
}
}
public class DiscountService {
private Discount discount;
public DiscountService(Discount discount) {
this.discount = discount;
}
public double apply(double price) {
return discount.apply(price);
}
}
Результат: Класс DiscountService не меняется — расширение происходит через добавление новых реализаций интерфейса.
3. Liskov Substitution Principle (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); // нарушение контракта
}
}
// Клиентский код
public void testArea(Rectangle rect) {
rect.setWidth(5);
rect.setHeight(10);
assert rect.getArea() == 50; // Для Square вернёт 100!
}
Правильное решение
Используем общий интерфейс без изменения поведения:
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) — принцип разделения интерфейсов
Суть: «Клиенты не должны зависеть от методов, которые они не используют». Лучше несколько узких интерфейсов, чем один «жирный».
Нарушение ISP
public interface Worker {
void work();
void eat();
void sleep();
}
public class Robot implements Worker {
@Override
public void work() { /* работает */ }
@Override
public void eat() {
throw new UnsupportedOperationException("Роботы не едят");
}
@Override
public void sleep() {
throw new UnsupportedOperationException("Роботы не спят");
}
}
Рефакторинг
public interface Workable {
void work