SOLID принципы в Java: теория и практика для разработчиков

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

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

Начать курс

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                

Блоги

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