Как вы применяете принципы SOLID в объектно-ориентированном дизайне?

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

Ответ

SOLID — это набор из пяти принципов, которые делают дизайн программ гибким, понятным и поддерживаемым.

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

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

Нарушение:

class Report {
    void generateContent() { /* ... */ }
    void formatToPDF() { /* ... */ } // Ответственность за форматирование
    void saveToFile(String path) { /* ... */ } // Ответственность за ввод-вывод
}

Следование:

class Report {
    void generateContent() { /* ... */ }
}
class ReportPDFFormatter {
    void format(Report report) { /* ... */ }
}
class ReportFileSaver {
    void save(Report report, String path) { /* ... */ }
}

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

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

Достигается через абстракции и полиморфизм:

interface DiscountStrategy {
    double apply(double price);
}

class RegularDiscount implements DiscountStrategy { /* ... */ }
class PremiumDiscount implements DiscountStrategy { /* ... */ }

class OrderProcessor {
    private DiscountStrategy discount;
    // Можно добавить новый тип скидки, НЕ изменяя OrderProcessor
    public OrderProcessor(DiscountStrategy discount) { this.discount = discount; }
    public double calculateTotal(double price) { return discount.apply(price); }
}

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

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

Нарушение:

class Rectangle {
    protected int width, height;
    void setWidth(int w) { width = w; }
    void setHeight(int h) { height = h; }
}
class Square extends Rectangle {
    @Override
    void setWidth(int w) { width = height = w; } // Меняет и высоту!
    @Override
    void setHeight(int h) { width = height = h; } // Меняет и ширину!
}
// Клиентский код ломается, ожидая независимого изменения сторон
void testArea(Rectangle r) {
    r.setWidth(5);
    r.setHeight(4);
    assert r.width * r.height == 20; // Упадет для Square
}

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

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

Плохо:

interface Worker {
    void work();
    void eat();
    void code(); // Не всем нужен
}
class Robot implements Worker {
    void work() { /* ... */ }
    void eat() { /* НЕ НУЖНО! */ } // Нарушение ISP
    void code() { /* ... */ }
}

Хорошо:

interface Workable { void work(); }
interface Eatable { void eat(); }
interface Codable { void code(); }

class Human implements Workable, Eatable, Codable { /* ... */ }
class Robot implements Workable, Codable { /* ... */ } // Только нужные интерфейсы

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

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

Достигается через Dependency Injection (DI):

// Уровень бизнес-логики (высокий) зависит от абстракции
interface Repository {
    void save(Data data);
}

class Service {
    private final Repository repository; // Зависимость от интерфейса
    public Service(Repository repository) { // Внедрение через конструктор
        this.repository = repository;
    }
    public void process(Data data) {
        // ... логика
        repository.save(data); // Не знает о конкретной реализации
    }
}

// Уровень доступа к данным (низкий) реализует абстракцию
class PostgresRepository implements Repository { /* ... */ }
class MongoRepository implements Repository { /* ... */ }

Итог: Применение SOLID ведет к созданию слабосвязанных (loosely coupled), тестируемых и легко расширяемых систем.