SOLID принципы в Java: от теории к практике
SOLID — это акроним пяти фундаментальных принципов объектно-ориентированного программирования, сформулированных Робертом Мартином (дядюшкой Бобом). Эти принципы помогают создавать гибкий, поддерживаемый и масштабируемый код. В этой статье мы разберем каждый принцип на примерах Java, покажем, как их нарушают, и как правильно применять.
Зачем нужны SOLID принципы?
Без SOLID код быстро превращается в «спагетти». Вы сталкивались с ситуацией, когда изменение одного класса ломает три других? Или когда метод делает слишком много? SOLID решает эти проблемы. Каждый принцип — это правило, которое делает код:
- Устойчивым к изменениям — новые требования не ломают старую логику.
- Понятным — каждый класс отвечает за одну вещь.
- Тестируемым — легко писать юнит-тесты.
1. Single Responsibility Principle (SRP) — Принцип единственной ответственности
Формулировка: «У класса должна быть только одна причина для изменения».
Простыми словами: класс должен делать только одну вещь. Если у класса больше одной ответственности, его сложно поддерживать.
Нарушение SRP (плохой код)
public class OrderService {
public void createOrder(Order order) {
// Логика создания заказа
saveToDatabase(order);
sendEmailConfirmation(order);
}
private void saveToDatabase(Order order) {
// Сохранение в БД
}
private void sendEmailConfirmation(Order order) {
// Отправка email
}
}
Проблема: класс отвечает и за бизнес-логику, и за работу с БД, и за отправку уведомлений. Изменение email-сервиса потребует изменения OrderService.
Правильная реализация SRP
public class OrderService {
private final OrderRepository repository;
private final EmailService emailService;
public OrderService(OrderRepository repo, EmailService email) {
this.repository = repo;
this.emailService = email;
}
public void createOrder(Order order) {
repository.save(order);
emailService.sendConfirmation(order);
}
}
public class OrderRepository {
public void save(Order order) { /* логика БД */ }
}
public class EmailService {
public void sendConfirmation(Order order) { /* логика email */ }
}
Теперь каждый класс имеет одну причину для изменения. OrderService меняется только при изменении бизнес-процесса создания заказа.
2. Open/Closed Principle (OCP) — Принцип открытости/закрытости
Формулировка: «Классы должны быть открыты для расширения, но закрыты для модификации».
Это означает, что вы должны добавлять новую функциональность через новый код, а не изменять существующий.
Нарушение OCP (плохой код)
public class AreaCalculator {
public double calculateArea(Object shape) {
if (shape instanceof Rectangle) {
Rectangle r = (Rectangle) shape;
return r.width * r.height;
} else if (shape instanceof Circle) {
Circle c = (Circle) shape;
return Math.PI * c.radius * c.radius;
}
throw new IllegalArgumentException("Unknown shape");
}
}
Чтобы добавить треугольник, нужно изменить метод calculateArea — это нарушает OCP.
Правильная реализация OCP
public interface Shape {
double calculateArea();
}
public class Rectangle implements Shape {
private double width, height;
public Rectangle(double w, double h) { this.width = w; this.height = h; }
@Override
public double calculateArea() { return width * height; }
}
public class Circle implements Shape {
private double radius;
public Circle(double r) { this.radius = r; }
@Override
public double calculateArea() { return Math.PI * radius * radius; }
}
public class AreaCalculator {
public double calculateArea(Shape shape) {
return shape.calculateArea();
}
}
Теперь для добавления новой фигуры достаточно создать класс, реализующий Shape. Существующий код не меняется.
3. Liskov Substitution Principle (LSP) — Принцип подстановки Барбары Лисков
Формулировка: «Объекты подкласса должны быть взаимозаменяемы с объектами суперкласса без нарушения корректности программы».
Если у вас есть класс Bird и подкласс Penguin, то код, работающий с Bird, должен корректно работать и с Penguin.
Нарушение LSP (плохой код)
public class Bird {
public void fly() { System.out.println("Flying"); }
}
public class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins can't fly!");
}
}
// Клиентский код
public void makeBirdFly(Bird bird) {
bird.fly(); // Упадет с ошибкой для Penguin
}
Пингвин — птица, но не летает. Наследование здесь нарушает LSP.
Правильная реализация LSP
public abstract class Bird {
public abstract void move();
}
public class FlyingBird extends Bird {
@Override
public void move() { System.out.println("Flying"); }
}
public class Penguin extends Bird {
@Override
public void move() { System.out.println("Swimming"); }
}
// Клиентский код
public void makeBirdMove(Bird bird) {
bird.move(); // Работает для всех птиц
}
Теперь подклассы корректно заменяют базовый класс.
4. Interface Segregation Principle (ISP) — Принцип разделения интерфейсов
Формулировка: «Клиенты не должны зависеть от методов, которые они не используют».
Лучше иметь несколько узкоспециализированных интерфейсов, чем один «толстый».
Нарушение ISP (плохой код)
public interface Worker {
void work();
void eat();
}
public class Robot implements Worker {
@Override
public void work() { System.out.println("Working"); }
@Override
public void eat() { throw new UnsupportedOperationException("Robots don't eat"); }
}
Робот вынужден реализовывать метод eat(), который ему не нужен.
Правильная реализация ISP
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public class Human implements Workable, Eatable {
@Override
public void work() { System.out.println("Working"); }
@Override
public void eat() { System.out.println("Eating"); }
}
public class Robot implements Workable {
@Override
public void work