SOLID принципы в Java: теория и продвинутая практика

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

SOLID принципы в Java: от теории к продвинутой практике

Принципы SOLID — это фундамент объектно-ориентированного проектирования, который помогает разработчикам создавать код, устойчивый к изменениям, легко тестируемый и масштабируемый. Для Java-разработчика понимание этих принципов на продвинутом уровне — это не просто знание определений, а умение видеть их нарушение в legacy-коде и применять паттерны для рефакторинга. В этой статье мы разберем каждый принцип с акцентом на сложные, но типичные ситуации.

1. Single Responsibility Principle (SRP): Грань между ответственностью и связностью

Теория: Класс должен иметь только одну причину для изменения. На практике это означает, что класс должен быть ответственен за одну часть функциональности программы.

Продвинутая практика: Часто нарушение SRP проявляется не в явном смешении логики, а в "размытой" ответственности. Рассмотрим пример класса для работы с отчетами:

// Нарушение SRP: класс занимается и данными, и форматированием, и отправкой
public class ReportService {
    public void generateReport(List<Data> data) {
        // Логика вычислений
        Map<String, Integer> aggregated = aggregate(data);
        // Форматирование (HTML)
        String html = formatToHtml(aggregated);
        // Отправка по 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) {
        // ...
    }
}

Проблема: если изменится формат отчета (нужен PDF вместо HTML), придется менять этот класс. Если изменится способ отправки (Telegram вместо email) — опять же. Решение — разбить на три класса:

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) { ... }
}

// Композиция в сервисе
public 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): Искусство расширения через абстракции

Теория: Классы должны быть открыты для расширения, но закрыты для модификации.

Продвинутая практика: Типичное нарушение — цепочки if-else или switch для разных типов объектов. Продвинутый способ — использование стратегии или шаблонного метода с полиморфизмом.

// Нарушение OCP: при добавлении нового типа платежа нужно менять класс
public class PaymentProcessor {
    public void processPayment(String type, double amount) {
        if ("CREDIT_CARD".equals(type)) {
            // логика для кредитной карты
        } else if ("PAYPAL".equals(type)) {
            // логика для PayPal
        } else if ("BITCOIN".equals(type)) {
            // логика для Bitcoin
        }
    }
}

Исправление через интерфейс и фабрику:

public interface PaymentMethod {
    void pay(double amount);
}

public class CreditCardPayment implements PaymentMethod {
    public void pay(double amount) { ... }
}

public class PayPalPayment implements PaymentMethod {
    public void pay(double amount) { ... }
}

// Класс закрыт для модификации, открыт для расширения
public class PaymentProcessor {
    private PaymentMethod method;

    public PaymentProcessor(PaymentMethod method) {
        this.method = method;
    }

    public void processPayment(double amount) {
        method.pay(amount);
    }
}

3. Liskov Substitution Principle (LSP) и Interface Segregation Principle (ISP): Тонкости наследования

Теория LSP: Подтипы должны быть заменяемы своими базовыми типами без нарушения работы программы.

Продвинутая практика: Классический пример — квадрат, наследующий от прямоугольника. В Java это часто проявляется в нарушении контрактов. Рассмотрим пример с коллекциями:

// Нарушение LSP
public class ImmutableList<E> extends ArrayList<E> {
    @Override
    public boolean add(E e) {
        throw new UnsupportedOperationException("Список неизменяем");
    }
}

// Клиентский код ожидает стандартного поведения ArrayList
public void addAll(List<String> list) {
    list.add("new item"); // Упадет с ошибкой, если передать ImmutableList
}

Правильное решение — не наследовать, а использовать композицию или делегирование. Или, если нужна неизменяемость, использовать Collections.unmodifiableList().

Теория ISP: Клиенты не должны зависеть от методов, которые они не используют.

Продвинутая практика: "Толстые" интерфейсы — бич больших систем. Допустим, у вас есть интерфейс Worker:

// Нарушение ISP
public interface Worker {
    void work();
    void eat();
    void sleep();
}

// Робот вынужден реализовывать методы eat() и sleep(), которые ему не нужны
public class Robot implements Worker {
    public void work() { ... }
    public void eat() { throw new UnsupportedOperationException(); }
    public void sleep() { throw new UnsupportedOperationException(); }
}

Решение — разделить на два интерфейса:

public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public interface Sleepable {
    void sleep();
}

public class HumanWorker implements Workable, Eatable, Sleepable { ... }
public class RobotWorker implements Workable { ... }

4. Dependency Inversion Principle (DIP): Инверсия зависимостей в реальных проектах

Теория: Модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те, и другие должны зависеть от абстракций. Абстракции не должны зависеть от деталей; детали должны зависеть от абстракций.

Продвинутая практика: Нарушение DIP — это когда класс бизнес-логики напрямую создает экземпляр конкретного репозитория или сервиса.

// Нарушение DIP
public class OrderService {
    private MySqlDatabase db = new MySqlDatabase(); // Жесткая связь                

Блоги

Рекомендаций по книгам