IT-журнал

Как применить принципы SOLID и писать устойчивый код

Узнайте, как использовать принципы SOLID в реальном коде, избегать типичных ошибок и улучшать архитектуру приложений. Пошаговые примеры и рекомендации.

Обновлено 21.09.2026

⏱ 3 мин чтения · 👁 прочитали 3

Введение

Принципы SOLID — набор рекомендаций, которые помогают создавать гибкие, расширяемые и поддерживаемые системы.
В статье разберём каждый принцип, покажем, как он выглядит в коде, и обсудим типичные подводные камни. После чтения вы сможете:

  • понять, когда и почему нужен каждый из пяти принципов;
  • трансформировать «запахи» кода в чистую архитектуру;
  • быстро оценить, нарушен ли SOLID в существующем проекте.

1. Обычное начало: «Код без SOLID»

Рассмотрим простой пример задачи: хранение и вывод информации о пользователях.

public class UserService {
    private final Database db = new Database();

    public String getUserInfo(int id) {
        User user = db.findUserById(id);
        if (user == null) {
            return "User not found";
        }
        return "User: " + user.getName() + ", email: " + user.getEmail();
    }

    public void exportUser(int id, String format) {
        User user = db.findUserById(id);
        if ("json".equals(format)) {
            // сериализация в JSON
        } else if ("xml".equals(format)) {
            // сериализация в XML
        }
    }
}

Код работает, но сразу бросаются в глаза проблемы:

  • UserService знает о конкретной базе данных — нарушение SRP (Single Responsibility Principle).
  • Добавление нового формата экспорта требует изменения метода exportUser — нарушение OCP (Open/Closed Principle).
  • Database создаётся внутри сервиса, что усложняет тестирование — нарушение DIP (Dependency Inversion Principle).

2. Принцип единственной ответственности (SRP)

Суть

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

Как исправить

Разделим бизнес‑логику, доступ к данным и экспорт в отдельные сервисы.

public interface UserRepository {
    User findById(int id);
}

public class SqlUserRepository implements UserRepository {
    private final Connection connection;
    public SqlUserRepository(Connection connection) { this.connection = connection; }
    public User findById(int id) { /* запрос к БД */ }
}

public interface UserExporter {
    String export(User user);
}

public class JsonUserExporter implements UserExporter {
    public String export(User user) {
        // сериализация в JSON
    }
}

UserService теперь только координирует действия:

public class UserService {
    private final UserRepository repo;
    private final UserExporter exporter;

    public UserService(UserRepository repo, UserExporter exporter) {
        this.repo = repo;
        this.exporter = exporter;
    }

    public String getUserInfo(int id) {
        User user = repo.findById(id);
        return user != null ? "User: " + user.getName() : "User not found";
    }

    public String exportUser(int id) {
        User user = repo.findById(id);
        return user != null ? exporter.export(user) : "";
    }
}

Плюс: каждый класс имеет одну очевидную задачу. Тестировать их отдельно стало проще.


3. Принцип открытости/закрытости (OCP)

Суть

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

Как реализовать

Добавим новый формат экспорта, не меняя существующий код UserService. Для этого используем фабрику или DI‑контейнер.

public interface UserExporterFactory {
    UserExporter create(String format);
}

public class DefaultUserExporterFactory implements UserExporterFactory {
    private final Map<String, UserExporter> exporters = new HashMap<>();

    public DefaultUserExporterFactory() {
        exporters.put("json", new JsonUserExporter());
        exporters.put("xml", new XmlUserExporter());
    }

    public UserExporter create(String format) {
        return exporters.getOrDefault(format, new JsonUserExporter());
    }
}

UserService теперь принимает фабрику:

public class UserService {
    private final UserRepository repo;
    private final UserExporterFactory exporterFactory;

    public UserService(UserRepository repo, UserExporterFactory exporterFactory) {
        this.repo = repo;
        this.exporterFactory = exporterFactory;
    }

    public String exportUser(int id, String format) {
        User user = repo.findById(id);
        return exporterFactory.create(format).export(user);
    }
}

Чтобы добавить CSV‑экспорт, достаточно реализовать CsvUserExporter и зарегистрировать его в фабрике — код UserService не меняется.


4. Принцип подстановки Барбары Лисков (LSP)

Суть

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

Пример с репозиторием

public abstract class BaseUserRepository implements UserRepository {
    public abstract User findById(int id);
}

public class CachedUserRepository extends BaseUserRepository {
    private final UserRepository inner;
    private final Map<Integer, User> cache = new HashMap<>();

    public CachedUserRepository(UserRepository inner) { this.inner = inner; }

    @Override
    public User findById(int id) {
        return cache.computeIfAbsent(id, inner::findById);
    }
}

CachedUserRepository полностью сохраняет контракт UserRepository. Клиент, получивший объект через интерфейс, не видит разницы — LSP соблюдён.

Частая ошибка: переопределять метод так, чтобы он бросал UnsupportedOperationException. Это нарушает LSP.


5. Принцип разделения интерфейса (ISP)

Суть

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

Как избавиться от «толстых» интерфейсов

Пусть у нас был единый интерфейс:

public interface UserOperations {
    User findById(int id);
    void deleteUser(int id);
    void updateUser(User user);
    String export(User user, String format);
}

Если часть системы нужна только чтение, а другая — только экспорт, разделим:

public interface UserReadRepository {
    User findById(int id);
}

public interface UserWriteRepository {
    void deleteUser(int id);
    void updateUser(User user);
}

public interface UserExportService {
    String export(User user, String format);
}

Теперь классы могут зависеть только от нужных им абстракций, а изменения в одной части не влияют на остальные.


6. Принцип инверсии зависимостей (DIP)

Суть

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

Реализация через DI‑контейнер

// Конфигурация (например, Spring)
@Bean
public UserRepository userRepository(Connection conn) {
    return new SqlUserRepository(conn);
}

@Bean
public UserExporterFactory exporterFactory() {
    return new DefaultUserExporterFactory();
}

@Bean
public UserService userService(UserRepository repo, UserExporterFactory factory) {
    return new UserService(repo, factory);
}

UserService получает зависимости в конструкторе, а не создает их самостоятельно. Это упрощает подмену реализаций в тестах:

UserRepository fakeRepo = new InMemoryUserRepository();
UserExporterFactory fakeFactory = new TestExporterFactory();
UserService service = new UserService(fakeRepo, fakeFactory);

7. Сводная таблица SOLID

Принцип Что гарантирует Как распознать нарушение
SRP Один класс — одну причину для изменения Класс растёт, включает разнородную логику
OCP Возможность расширять без изменения Нужно править существующий код, чтобы добавить фичу
LSP Подкласс совместим с базовым Подкласс бросает исключения, меняет сигнатуру
ISP Клиенты получают только нужный набор методов «Тучные» интерфейсы, методы, не используемые клиентом
DIP Высокоуровневые модули зависят от абстракций Конкретные реализации создаются внутри бизнес‑классов

8. Типичные «грабли» и как их обходить

Грабль Причина Как избежать
Крупные сервисы Нарушение SRP Делить сервисы по бизнес‑операциям, использовать фасады
Дублирование логики в разных репозиториях Нарушение DRY и ISP Вынести общую часть в базовый класс или отдельный компонент
Бросание UnsupportedOperationException Нарушение LSP Пересмотреть иерархию, создать более узкие интерфейсы
Жёсткое связывание с конкретными классами Нарушение DIP Внедрять зависимости через конструктор/интерфейс
Добавление нового формата через условный оператор Нарушение OCP Использовать паттерн Strategy или фабрику

9. Альтернативы и дополнения

  • KISS (Keep It Simple, Stupid) — помогает не перегружать абстракциями.
  • DRY (Don’t Repeat Yourself) — комплементарен ISP и SRP.
  • Clean Architecture (Robert C. Martin) — описывает слоёную структуру, где SOLID‑принципы естественно вписываются.
  • Domain‑Driven Design — часто использует отдельные агрегаты, где каждый агрегат следует SRP и OCP.

Вывод

  • SOLID — не набор правил, а набор проверенных шаблонов, помогающих писать код, который легко менять и тестировать.
  • Применяйте каждый принцип осознанно: не стоит «внедрять» OCP в каждом мелком классе, если это приводит к избыточной абстракции.
  • При проектировании ориентируйтесь на читаемость и поддерживаемость. Если вы чувствуете, что один класс растёт, подумайте о SRP; если добавление новой функции требует правки старых модулей — ищите OCP‑решения.
  • Комбинируйте SOLID с другими практиками (KISS, DRY, Тест‑первой разработки) — получаете более надёжную и гибкую архитектуру.

Следуйте этим рекомендациям, и ваш код будет легче поддерживаться, расширяться и тестироваться. 🚀