Приведите пример нарушения принципа Single Responsibility
Представьте шеф-повара элитного ресторана, который одновременно: 4. Моет полы в зале.
🟢 Junior Level
Нарушение принципа Single Responsibility (SRP) происходит, когда класс берет на себя управление несколькими несвязанными задачами, заставляя его меняться по разным, независимым друг от друга причинам.
Класс-нарушитель (God Object / Swiss Army Knife):
┌────────────────────────────────────────────────────────┐
│ OrderController │
│ ├── parseAndValidateJson() ──► Работа с HTTP/JSON │
│ ├── calculateDiscounts() ──► Бизнес-логика продаж │
│ ├── executeSqlInsert() ──► Доступ к данным / SQL │
│ └── sendSmsViaTwilio() ──► Интеграция с SMS-шлюзом│
└────────────────────────────────────────────────────────┘
Наглядный пример из жизни:
Представьте шеф-повара элитного ресторана, который одновременно:
- Готовит блюда на кухне.
- Сам развозит заказы на мопеде по городу.
- Ведет налоговый учет и сдаёт бухгалтерскую отчетность.
- Моет полы в зале.
Если шеф-повар попадет в пробку при доставке, кухня встанет, а бухгалтерия получит штрафы. В правильном ресторане каждый занимается своим делом: повар — кухней, курьер — доставкой, бухгалтер — налогами.
Классический антипаттерн в Java:
// ПЛОХО: Один класс совмещает 4 разные ответственности
public class Invoice {
private Long id;
private BigDecimal amount;
// 1. Бизнес-логика расчета налога
public BigDecimal calculateTax() {
return amount.multiply(new BigDecimal("0.20"));
}
// 2. Логика персистентности (SQL / JDBC)
public void saveToDatabase() {
String sql = "INSERT INTO invoices (id, amount) VALUES (?, ?)";
// Открытие Connection, PreparedStatement, executeUpdate...
}
// 3. Форматирование представления (HTML / PDF)
public String generatePdfReport() {
return "<html><body><h1>Счет #" + id + "</h1></body></html>";
}
// 4. Коммуникация / Отправка
public void emailToCustomer(String email) {
// Настройка SMTP сессии, Transport.send...
}
}
🟡 Middle Level
1. Реальные примеры нарушения SRP в Spring Boot
Пример 1: «Всеядный» Spring @Service
Самый распространенный антипаттерн в enterprise-разработке — превращение сервиса в мусорную свалку из 15 репозиториев:
// ПЛОХО: GeneralUserService знает обо всем на свете
@Service
@RequiredArgsConstructor
public class GeneralUserService {
private final UserRepository userRepo;
private final OrderRepository orderRepo;
private final EmailClient emailClient;
private final PasswordEncoder encoder;
private final S3Client s3Client;
private final AuditLogger auditLogger;
// Причина 1: Сменились правила валидации паролей (безопасность)
public void registerUser(UserDto dto) { ... }
// Причина 2: Сменился провайдер облачного хранилища S3 (DevOps)
public void uploadAvatar(Long userId, byte[] image) { ... }
// Причина 3: Изменился шаблон письма для сброса пароля (маркетинг)
public void sendPasswordResetEmail(String email) { ... }
// Причина 4: Бухгалтерия изменила формат выгрузки истории заказов (финансы)
public byte[] exportOrderHistoryToExcel(Long userId) { ... }
}
Пример 2: Совмещение JPA Entity и DTO
// ПЛОХО: Сущность базы данных одновременно является DTO для REST API
@Entity
@Table(name = "users")
public class User {
@Id
private Long id;
@Column(name = "db_password_hash")
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY) // Детали JSON-сериализации!
private String password;
@Transient
@Pattern(regexp = "^\\+?[0-9]{10,13}$") // Валидация входящего HTTP-запроса!
private String rawPhoneInput;
}
В чем ошибка: Изменение контракта публичного фронтенда ломает схему таблицы в PostgreSQL, и наоборот.
🔴 Senior Level
1. Разрушительные последствия нарушения SRP
Нарушение SRP
├──► Хрупкость (Fragility): Исправление логики отправки email ломает расчет скидок
├──► Неподвижность (Immobility): Нельзя переиспользовать метод без затягивания 15 чужих зависимостей
├──► Проблема слияния (Merge Conflicts): 10 разработчиков одновременно правят один God-класс
└──► Утечка транзакционных ресурсов (Connection Starvation):
Опасность Connection Starvation при нарушении SRP:
Если в одном транзакционном методе сервиса смешана работа с базой данных и медленный внешний HTTP-вызов:
@Transactional // Открывает соединение с БД из пула HikariCP!
public void processOrder(OrderDto dto) {
Order order = orderRepo.save(new Order(dto)); // 1. Быстрый INSERT (2 мс)
// 2. Вызов внешнего сервиса эквайринга ПО СЕТИ внутри транзакции!
// Если банк тормозит 5 секунд — поток все 5 секунд держит соединение к БД!
bankPaymentGateway.charge(dto.getCard(), dto.getAmount());
order.setStatus(OrderStatus.PAID);
orderRepo.save(order);
}
При 100 одновременных запросах пул соединений базы данных (maximum-pool-size = 30) мгновенно истощается. Вся база данных парализована из-за того, что транзакционный метод нарушил SRP и объединил персистентность с сетевым I/O.
2. Рефакторинг: Decorator вместо раздувания сервиса
Как добавить в сервис кэширование, аудит и сбор метрик без нарушения SRP?
- ❌ Плохо: Внедрить
MeterRegistryиCacheManagerпрямо вOrderService(класс начинает отвечать за бизнес-логику + мониторинг + кэш). - ✅ Хорошо: Использовать паттерн Decorator (Декоратор):
// 1. Чистый контракт
public interface OrderService {
Order createOrder(OrderRequest req);
}
// 2. Только чистая бизнес-логика (SRP)
@Service
@Primary
public class CoreOrderService implements OrderService {
@Override
public Order createOrder(OrderRequest req) {
// Только создание и бизнес-правила заказа
return new Order(req);
}
}
// 3. Декоратор метрик: отвечает ТОЛЬКО за метрики (SRP)
@Service
@RequiredArgsConstructor
public class TimedOrderServiceDecorator implements OrderService {
private final OrderService delegate;
private final MeterRegistry registry;
@Override
public Order createOrder(OrderRequest req) {
return registry.timer("order.creation.time")
.record(() -> delegate.createOrder(req));
}
}
4 Tricky Questions
1. Как совмещение @Entity (JPA) и @JsonProperty (Jackson) в одном классе нарушает SRP и к каким проблемам это приводит?
Ответ: Класс берет на себя две принципиально разные ответственности:
- Отображение на схему реляционной БД (Domain/Data model) — меняется при миграциях СУБД, добавлении индексов, связей
@OneToMany. - Публичный контракт внешнего API (Presentation/Transport model) — меняется при изменении требований мобильных клиентов и фронтенда.
Проблемы на практике:
- Утечка чувствительных данных (Over-fetching / Security Leak): Случайно добавленное в Entity поле
passwordHashилиinternalAuditIdавтоматически утекает в JSON-ответ клиенту, если разработчик забыл повесить@JsonIgnore. - Циклическая рекурсия: Двунаправленные связи (
@ManyToOneи@OneToMany) приводят кStackOverflowErrorпри попытке сериализации в JSON. Решение: Строгое разделение наOrderEntity(для БД) иOrderResponseDto(для REST API) с маппингом через MapStruct.
- Утечка чувствительных данных (Over-fetching / Security Leak): Случайно добавленное в Entity поле
2. Что такое Characterization Tests (тесты фиксации поведения / Golden Master) и как они применяются при распиле God Object?
Ответ: При рефакторинге унаследованного «божественного класса» на 5000 строк Unit-тесты часто полностью отсутствуют. Писать классические Unit-тесты невозможно, так как код запутан и содержит скрытые побочные эффекты. Методика Golden Master:
- Классу скармливается репрезентативный набор разнообразных входных данных (1000 разных запросов).
- Выходные результаты (состояние БД, возвращенные строки, сгенерированные файлы) записываются в файл-эталон (Golden Master).
- Создается автотест, проверяющий, что при любых изменениях код возвращает в точности зафиксированный результат.
- Под защитой Golden Master разработчики безопасно извлекают методы в отдельные SRP-классы.
3. Почему в архитектуре CQRS разделение сущности на OrderCommandService и OrderQueryService считается торжеством SRP?
Ответ: Чтение (Query) и запись (Command) имеют принципиально разные требования:
- Запись: Требует строгих бизнес-инвариантов, блокировок, транзакций, нормализации данных и валидаций. Заказчик — доменные бизнес-эксперты.
- Чтение: Требует высокой скорости, денормализации, кэширования, сложных агрегаций и пагинации для UI. Заказчик — UI/UX дизайнеры. Объединение их в один класс заставляет его меняться и при добавлении фильтра на экране каталога, и при изменении налоговой логики. Разделение на Command и Query дает каждому сервису ровно одну причину для изменения.
4. В чем разница между Cohesion (связностью) и Coupling (зацеплением) на примере класса, нарушающего SRP?
Ответ:
- Cohesion (Внутренняя связность): Показывает, насколько части внутри одного класса сфокусированы на выполнении одной задачи. В классе-нарушителе SRP связность экстремально низкая (Low Cohesion): метод
calculateSalary()никак не связан с методомsendEmail(), они оперируют совершенно разными переменными и не имеют общей цели. - Coupling (Внешнее зацепление): Показывает, насколько сильно класс зависит от других классов системы. У нарушителя SRP зацепление экстремально высокое (High Coupling): ему требуются десятки внешних зависимостей (
UserRepository,EmailSender,S3Client,PdfGenerator), и поломка любой из них ломает весь класс.
🎯 Шпаргалка для интервью
Главное:
- Нарушение SRP возникает, когда класс выполняет несколько разнородных задач и меняется по требованиям разных стейкхолдеров.
- Классические нарушители: God Object, сервисы на 2000+ строк, Entity с Jackson-аннотациями, методы со смешением SQL и HTTP.
- Опаснейшее последствие в Spring: Connection Pool Starvation, если внутри
@Transactionalвызывается медленный сторонний API. - Для добавления сквозной функциональности (логи, кэш, метрики) используйте Decorator или Spring AOP, а не раздувайте класс.
- Разделяйте Entity (база) и DTO (API).
- Признаки нарушения: конструктор с 10+
@Autowired, в тестах больше 5 моков, циклыif-elseна все случаи жизни.
Красные флаги (Чего говорить НЕЛЬЗЯ):
- ❌ «Совмещать Entity и DTO в одном классе — это нормальная оптимизация, чтобы не писать лишние классы» (Приводит к утечкам данных, StackOverflowError и жесткой связности базы с API).
- ❌ «В методе
@Transactionalнормально делать вызовы сторонних платежных систем по HTTP» (Удерживает соединение с БД и приводит к отказу СУБД). - ❌ «Если класс имеет 20 методов, он автоматически нарушает SRP» (Если все 20 методов служат одной цели, например
BigDecimal, SRP соблюден).