Каковы основные принципы SOLID в Java?

«Каковы основные принципы SOLID в Java?» — вопрос из категории Основы программирования, который задают на 10% собеседований QA Тестировщик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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

1. Принцип единственной ответственности (Single Responsibility Principle - SRP)

Один класс должен иметь одну и только одну причину для изменения.

Класс должен отвечать за одну конкретную задачу или функциональность.

Пример нарушения и исправления:

// НАРУШЕНИЕ: Класс отвечает и за работу с данными, и за логирование.
class UserService {
    public void saveUser(User user) {
        // ... сохранение в БД
    }
    public void logError(String message) {
        // ... запись в лог-файл
    }
}

// СОБЛЮДЕНИЕ: Ответственности разделены.
class UserRepository { // Отвечает только за данные
    public void save(User user) { /*...*/ }
}

class Logger { // Отвечает только за логирование
    public void log(String message) { /*...*/ }
}

class UserService { // Координирует работу
    private UserRepository repository;
    private Logger logger;
    public void saveUser(User user) {
        try {
            repository.save(user);
        } catch (Exception e) {
            logger.log(e.getMessage());
        }
    }
}

2. Принцип открытости/закрытости (Open/Closed Principle - OCP)

Программные сущности должны быть открыты для расширения, но закрыты для модификации.

Новую функциональность следует добавлять путем создания нового кода, а не изменяя существующий.

Пример: Использование интерфейсов и полиморфизма для добавления новых типов фигур без изменения кода, который вычисляет общую площадь.

3. Принцип подстановки Барбары Лисков (Liskov Substitution Principle - LSP)

Объекты в программе должны быть заменяемыми на экземпляры их подтипов без изменения правильности программы.

Наследующий класс должен дополнять, а не изменять поведение базового класса.

Пример нарушения: Класс Square, наследующий от Rectangle. Если у Rectangle есть сеттеры для ширины и высоты по отдельности, то у Square их изменение нарушит инвариант (равенство сторон). Это нарушает LSP, так как код, работающий с Rectangle, сломается при передаче Square.

4. Принцип разделения интерфейса (Interface Segregation Principle - ISP)

Много специализированных интерфейсов лучше, чем один универсальный.

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

Пример:

// ПЛОХО: "Толстый" интерфейс
interface Worker {
    void work();
    void eat();
    void sleep();
}
// Класс Robot вынужден реализовывать eat() и sleep(), которые ему не нужны.

// ХОРОШО: Разделенные интерфейсы
interface Workable {
    void work();
}
interface Eatable {
    void eat();
}
interface Sleepable {
    void sleep();
}

class Human implements Workable, Eatable, Sleepable { /*...*/ }
class Robot implements Workable { /*...*/ } // Реализует только нужное

5. Принцип инверсии зависимостей (Dependency Inversion Principle - DIP)

Зависимости должны строиться на абстракциях, а не на деталях.

  1. Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций.
  2. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Пример (внедрение зависимости через конструктор):

// Абстракция (интерфейс)
interface MessageService {
    void sendMessage(String message);
}

// Деталь реализации 1
class EmailService implements MessageService {
    public void sendMessage(String message) { /* отправка email */ }
}

// Деталь реализации 2
class SMSService implements MessageService {
    public void sendMessage(String message) { /* отправка SMS */ }
}

// Модуль верхнего уровня зависит от абстракции, а не от конкретного сервиса.
class NotificationService {
    private MessageService service;
    // Внедрение зависимости (Dependency Injection)
    public NotificationService(MessageService service) {
        this.service = service;
    }
    public void notify(String message) {
        service.sendMessage(message);
    }
}
// Теперь NotificationService легко тестировать с Mock-объектом и менять реализацию MessageService.

Следование SOLID облегчает модульное тестирование, рефакторинг и снижает связанность компонентов системы.