← Назад к вопросам

Что такое принцип единственной ответственности (SRP, Single Responsibility Principle) в ООП?

1.7 Middle🔥 231 комментариев
#SOLID и паттерны проектирования

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Ответ

Принцип единственной ответственности (Single Responsibility Principle, SRP) — один из пяти SOLID принципов. Он гласит, что класс должен иметь одну и только одну причину для изменения. Другими словами, класс должен отвечать только за одно.

1. Основная идея SRP

// НЕПРАВИЛЬНО: Класс имеет множество ответственностей
public class User {
    private String name;
    private String email;
    private String password;
    
    // Ответственность 1: Управление данными пользователя
    public void setName(String name) {
        this.name = name;
    }
    
    // Ответственность 2: Проверка email
    public boolean isValidEmail() {
        return email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
    }
    
    // Ответственность 3: Хеширование пароля
    public String hashPassword(String password) {
        return new BCryptPasswordEncoder().encode(password);
    }
    
    // Ответственность 4: Отправка email
    public void sendWelcomeEmail() {
        // логика отправки email
    }
    
    // Ответственность 5: Логирование
    public void logActivity() {
        System.out.println("User activity: " + this.name);
    }
    
    // Ответственность 6: Взаимодействие с БД
    public void saveToDatabase() {
        // SQL запросы
    }
}

Проблемы:

  • Класс слишком сложный
  • Сложно тестировать
  • Изменение email валидации требует изменения класса User
  • Изменение логирования требует изменения класса User

2. Правильное применение SRP

Разделяем на несколько классов, каждый с одной ответственностью:

// Класс 1: Управление данными пользователя
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private String id;
    
    @Column(name = "name")
    private String name;
    
    @Column(name = "email")
    private String email;
    
    @Column(name = "password_hash")
    private String password;
    
    // Только getters и setters — нет логики
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

// Класс 2: Валидация email
public class EmailValidator {
    public boolean isValid(String email) {
        return email != null && email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
    }
}

// Класс 3: Работа с паролями
@Component
public class PasswordService {
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    public String hashPassword(String plainPassword) {
        return passwordEncoder.encode(plainPassword);
    }
    
    public boolean isPasswordValid(String plainPassword, String hash) {
        return passwordEncoder.matches(plainPassword, hash);
    }
}

// Класс 4: Отправка email
@Service
public class EmailService {
    @Autowired
    private JavaMailSender mailSender;
    
    public void sendWelcomeEmail(String to, String name) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(to);
        message.setSubject("Welcome " + name);
        message.setText("Welcome to our service!");
        mailSender.send(message);
    }
}

// Класс 5: Логирование
@Component
public class UserActivityLogger {
    private static final Logger logger = LoggerFactory.getLogger(UserActivityLogger.class);
    
    public void logUserActivity(String username, String activity) {
        logger.info("User {} performed: {}", username, activity);
    }
}

// Класс 6: Работа с БД
@Repository
public interface UserRepository extends JpaRepository<User, String> {
    Optional<User> findByEmail(String email);
}

// Класс 7: Ввод-вывод (Controller)
@RestController
@RequestMapping("/api/users")
public class UserController {
    @Autowired
    private UserService userService;
    
    @PostMapping
    public ResponseEntity<?> createUser(@RequestBody CreateUserRequest request) {
        return ResponseEntity.ok(userService.createUser(request));
    }
}

// Оркестратор: Координирует работу всех компонентов
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private EmailValidator emailValidator;
    
    @Autowired
    private PasswordService passwordService;
    
    @Autowired
    private EmailService emailService;
    
    @Autowired
    private UserActivityLogger logger;
    
    @Transactional
    public User createUser(CreateUserRequest request) {
        // Валидация email
        if (!emailValidator.isValid(request.getEmail())) {
            throw new IllegalArgumentException("Invalid email");
        }
        
        // Хеширование пароля
        String hashedPassword = passwordService.hashPassword(request.getPassword());
        
        // Создание пользователя
        User user = new User();
        user.setName(request.getName());
        user.setEmail(request.getEmail());
        user.setPassword(hashedPassword);
        
        // Сохранение в БД
        User savedUser = userRepository.save(user);
        
        // Отправка email
        emailService.sendWelcomeEmail(user.getEmail(), user.getName());
        
        // Логирование
        logger.logUserActivity(user.getName(), "User registered");
        
        return savedUser;
    }
}

public class CreateUserRequest {
    private String name;
    private String email;
    private String password;
    // getters
}

3. Архитектурный подход

У каждого слоя своя ответственность:

┌─────────────────────────────────────┐
│   Presentation (Controller)         │ ← Обработка HTTP запросов
├─────────────────────────────────────┤
│   Application (Service)             │ ← Бизнес-логика
├─────────────────────────────────────┤
│   Domain (Entities, ValueObjects)   │ ← Модели данных
├─────────────────────────────────────┤
│   Infrastructure (Repository, DB)   │ ← Работа с БД
└─────────────────────────────────────┘
// Domain слой: только сущности
public class User {
    private String id;
    private String name;
    // Только данные
}

// Application слой: бизнес-логика
@Service
public class UserService {
    // Координирует взаимодействие
}

// Infrastructure слой: работа с технологиями
@Repository
public interface UserRepository extends JpaRepository<User, String> {
    // Работа с БД
}

4. Практический пример с PaymentProcessor

// НЕПРАВИЛЬНО: Одна большая ответственность
public class PaymentProcessor {
    public void processPayment(Order order) {
        // Валидация
        if (order.getTotal() <= 0) {
            throw new IllegalArgumentException();
        }
        
        // Вызов API платёжной системы
        String response = callPaymentAPI(order);
        
        // Парсинг ответа
        PaymentResult result = parseResponse(response);
        
        // Обновление заказа
        order.setStatus("paid");
        
        // Отправка email
        sendPaymentConfirmationEmail(order);
        
        // Логирование
        logPayment(order, result);
    }
}

Правильно: Разделяем ответственности

// Класс 1: Валидация платежей
public class PaymentValidator {
    public void validate(Order order) throws ValidationException {
        if (order.getTotal() <= 0) {
            throw new ValidationException("Invalid amount");
        }
    }
}

// Класс 2: Интеграция с платёжной системой
@Component
public class PaymentGateway {
    public PaymentResult process(Order order) {
        String response = callPaymentAPI(order);
        return parseResponse(response);
    }
}

// Класс 3: Обновление статуса заказа
@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;
    
    public void markAsPaid(Order order) {
        order.setStatus("paid");
        order.setPaidAt(LocalDateTime.now());
        orderRepository.save(order);
    }
}

// Класс 4: Уведомления
@Service
public class NotificationService {
    public void sendPaymentConfirmation(Order order) {
        // Отправка email
    }
}

// Класс 5: Логирование платежей
@Component
public class PaymentLogger {
    private static final Logger logger = LoggerFactory.getLogger(PaymentLogger.class);
    
    public void logSuccess(Order order, PaymentResult result) {
        logger.info("Payment processed for order {}", order.getId());
    }
}

// Оркестратор: координирует работу
@Service
public class PaymentService {
    @Autowired
    private PaymentValidator validator;
    
    @Autowired
    private PaymentGateway gateway;
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private NotificationService notificationService;
    
    @Autowired
    private PaymentLogger logger;
    
    @Transactional
    public void processPayment(Order order) {
        validator.validate(order);
        PaymentResult result = gateway.process(order);
        orderService.markAsPaid(order);
        notificationService.sendPaymentConfirmation(order);
        logger.logSuccess(order, result);
    }
}

5. Правила идентификации нарушения SRP

// ПРИЗНАК: "and" в названии класса
public class UserAndAuthenticationManager { }  // ❌ Нарушает SRP

// ПРИЗНАК: "or" в описании ответственности
// "Класс отвечает за управление пользователем ИЛИ аутентификацию"

// ПРИЗНАК: Много импортов разных систем
public class UserProcessor {
    private EmailService emailService;  // email
    private SmsService smsService;      // SMS
    private LogService logService;      // логирование
    private PaymentService payment;     // платежи
    // Слишком много ответственностей!
}

// ПРИЗНАК: Класс изменяется по разным причинам
// Изменение email логики → User изменяется
// Изменение логирования → User изменяется
// Изменение паролей → User изменяется

6. Тестирование с применением SRP

@SpringBootTest
public class PasswordServiceTest {
    @Autowired
    private PasswordService passwordService;  // Тестирует только пароли
    
    @Test
    public void testPasswordHashing() {
        String password = "Test123!";
        String hash = passwordService.hashPassword(password);
        assertTrue(passwordService.isPasswordValid(password, hash));
    }
}

@SpringBootTest
public class EmailValidatorTest {
    private EmailValidator validator = new EmailValidator();
    
    @Test
    public void testValidEmail() {
        assertTrue(validator.isValid("user@example.com"));
        assertFalse(validator.isValid("invalid.email"));
    }
}

@SpringBootTest
public class UserServiceTest {
    @MockBean
    private UserRepository userRepository;
    
    @MockBean
    private EmailValidator emailValidator;
    
    @MockBean
    private EmailService emailService;
    
    @Autowired
    private UserService userService;  // Тестируем только оркестрацию
    
    @Test
    public void testCreateUser() {
        // Легко мокировать, так как зависимости разделены
    }
}

Преимущества SRP

  1. Легче тестировать — каждый класс делает одно
  2. Проще поддерживать — изменения локализованы
  3. Переиспользование — классы можно использовать в разных контекстах
  4. Читаемость — код понятнее
  5. Меньше зависимостей — каждый класс зависит только от нужного

Антипаттерны

// God Object — делает всё
public class Application {  // ❌
    public void handleHttp() { }
    public void processDatabase() { }
    public void sendEmails() { }
    public void runBusinessLogic() { }
}

// Feature Envy — класс слишком интересуется другим классом
public class OrderProcessor {
    public void process(Order order, User user) {
        // Много обращений к User
        user.getEmail();          // ❌
        user.getPhoneNumber();    // ❌
        user.getAddress();        // ❌
        // Лучше передать UserDTO
    }
}

Вывод

SRP — это не просто принцип, это инвестиция в качество кода. Класс должен иметь одну причину для изменения, что делает код:

  • Проще в тестировании
  • Проще в поддержке
  • Более модульным и гибким