# Принципы проектирования ## Навигация - [🏠 Главная страница курса](../../../README.md) - [📚 Модуль 1](../README.md) --- ## SOLID принципы ### 1. Single Responsibility Principle (SRP) **Принцип единственной ответственности** > Класс должен иметь только одну причину для изменения **Пример нарушения:** ```java // Нарушение SRP - класс делает слишком много class UserManager { private UserRepository userRepository; private EmailService emailService; private PasswordEncoder passwordEncoder; private AuditService auditService; public User createUser(CreateUserRequest request) { // 1. Валидация данных if (request.getEmail() == null || !isValidEmail(request.getEmail())) { throw new ValidationException("Invalid email"); } // 2. Хеширование пароля String hashedPassword = passwordEncoder.encode(request.getPassword()); // 3. Создание пользователя User user = new User(request.getName(), request.getEmail(), hashedPassword); user = userRepository.save(user); // 4. Отправка приветственного email emailService.sendWelcomeEmail(user.getEmail()); // 5. Аудит auditService.logUserCreation(user.getId()); return user; } public void updateUser(Long userId, UpdateUserRequest request) { // Аналогично много ответственностей... } public void deleteUser(Long userId) { // Аналогично много ответственностей... } } ``` **Правильная реализация:** ```java // Разделение ответственностей class User { private Long id; private String name; private String email; private String passwordHash; // Только данные пользователя и бизнес-логика public void changePassword(String newPassword, PasswordEncoder encoder) { this.passwordHash = encoder.encode(newPassword); } public boolean isEmailValid() { return email != null && email.contains("@"); } } class UserValidator { public void validateCreateRequest(CreateUserRequest request) { if (request.getName() == null || request.getName().trim().isEmpty()) { throw new ValidationException("Name is required"); } if (request.getEmail() == null || !isValidEmail(request.getEmail())) { throw new ValidationException("Invalid email"); } if (request.getPassword() == null || request.getPassword().length() < 8) { throw new ValidationException("Password must be at least 8 characters"); } } private boolean isValidEmail(String email) { return email.matches("^[A-Za-z0-9+_.-]+@(.+)$"); } } class UserRepository { public User save(User user) { // Только операции с БД return userRepository.save(user); } public Optional findById(Long id) { return userRepository.findById(id); } } class UserNotificationService { public void sendWelcomeEmail(String email) { // Только отправка уведомлений emailService.sendWelcomeEmail(email); } } class UserAuditService { public void logUserCreation(Long userId) { // Только аудит auditService.logUserCreation(userId); } } // Координатор, который использует все сервисы class UserService { private UserValidator validator; private UserRepository repository; private UserNotificationService notificationService; private UserAuditService auditService; private PasswordEncoder passwordEncoder; public User createUser(CreateUserRequest request) { // 1. Валидация validator.validateCreateRequest(request); // 2. Создание пользователя String hashedPassword = passwordEncoder.encode(request.getPassword()); User user = new User(request.getName(), request.getEmail(), hashedPassword); // 3. Сохранение user = repository.save(user); // 4. Уведомления notificationService.sendWelcomeEmail(user.getEmail()); // 5. Аудит auditService.logUserCreation(user.getId()); return user; } } ``` ### 2. Open/Closed Principle (OCP) **Принцип открытости/закрытости** > Программные сущности должны быть открыты для расширения, но закрыты для модификации **Пример нарушения:** ```java // Нарушение OCP - нужно модифицировать класс для добавления новых типов class OrderProcessor { public void processOrder(Order order) { switch (order.getPaymentType()) { case CREDIT_CARD: processCreditCardPayment(order); break; case PAYPAL: processPayPalPayment(order); break; case BANK_TRANSFER: processBankTransferPayment(order); break; // При добавлении нового типа платежа нужно модифицировать этот класс } } private void processCreditCardPayment(Order order) { // логика обработки кредитной карты } private void processPayPalPayment(Order order) { // логика обработки PayPal } private void processBankTransferPayment(Order order) { // логика обработки банковского перевода } } ``` **Правильная реализация:** ```java // Соблюдение OCP - открыто для расширения, закрыто для модификации interface PaymentProcessor { boolean canProcess(PaymentType paymentType); PaymentResult processPayment(PaymentRequest request); } class CreditCardProcessor implements PaymentProcessor { @Override public boolean canProcess(PaymentType paymentType) { return paymentType == PaymentType.CREDIT_CARD; } @Override public PaymentResult processPayment(PaymentRequest request) { // логика обработки кредитной карты return new PaymentResult(true, "Payment processed via credit card"); } } class PayPalProcessor implements PaymentProcessor { @Override public boolean canProcess(PaymentType paymentType) { return paymentType == PaymentType.PAYPAL; } @Override public PaymentResult processPayment(PaymentRequest request) { // логика обработки PayPal return new PaymentResult(true, "Payment processed via PayPal"); } } class CryptocurrencyProcessor implements PaymentProcessor { @Override public boolean canProcess(PaymentType paymentType) { return paymentType == PaymentType.CRYPTOCURRENCY; } @Override public PaymentResult processPayment(PaymentRequest request) { // логика обработки криптовалюты return new PaymentResult(true, "Payment processed via cryptocurrency"); } } // Основной класс не нужно модифицировать для добавления новых процессоров class OrderProcessor { private List processors; public OrderProcessor(List processors) { this.processors = processors; } public PaymentResult processOrder(Order order) { PaymentProcessor processor = processors.stream() .filter(p -> p.canProcess(order.getPaymentType())) .findFirst() .orElseThrow(() -> new UnsupportedPaymentTypeException( "No processor found for payment type: " + order.getPaymentType())); PaymentRequest request = new PaymentRequest(order.getAmount(), order.getPaymentDetails()); return processor.processPayment(request); } } // Конфигурация процессоров @Configuration public class PaymentProcessorConfig { @Bean public List paymentProcessors() { return Arrays.asList( new CreditCardProcessor(), new PayPalProcessor(), new CryptocurrencyProcessor() // Новый процессор добавляется без модификации существующего кода ); } } ``` ### 3. Liskov Substitution Principle (LSP) **Принцип подстановки Лисков** > Объекты производного класса должны быть заменяемы объектами базового класса **Пример нарушения:** ```java // Нарушение LSP class Rectangle { protected int width; protected int height; public void setWidth(int width) { this.width = width; } public void setHeight(int height) { this.height = height; } public int getArea() { return width * height; } } class Square extends Rectangle { @Override public void setWidth(int width) { this.width = width; this.height = width; // Нарушение LSP - изменяет поведение } @Override public void setHeight(int height) { this.height = height; this.width = height; // Нарушение LSP - изменяет поведение } } // Проблема: код, работающий с Rectangle, может сломаться с Square public void testRectangle(Rectangle rectangle) { rectangle.setWidth(5); rectangle.setHeight(4); assert rectangle.getArea() == 20; // Для Square это будет 16! } ``` **Правильная реализация:** ```java // Соблюдение LSP abstract class Shape { public abstract int getArea(); } class Rectangle extends Shape { private int width; private int height; public Rectangle(int width, int height) { this.width = width; this.height = height; } public void setWidth(int width) { this.width = width; } public void setHeight(int height) { this.height = height; } @Override public int getArea() { return width * height; } } class Square extends Shape { private int side; public Square(int side) { this.side = side; } public void setSide(int side) { this.side = side; } @Override public int getArea() { return side * side; } } // Теперь любой Shape можно заменить другим Shape public void testShape(Shape shape) { int area = shape.getArea(); // Логика работает корректно для любого типа Shape } ``` ### 4. Interface Segregation Principle (ISP) **Принцип разделения интерфейсов** > Клиенты не должны зависеть от интерфейсов, которые они не используют **Пример нарушения:** ```java // Нарушение ISP - слишком большой интерфейс interface Worker { void work(); void eat(); void sleep(); void code(); void design(); void test(); void deploy(); } class Developer implements Worker { @Override public void work() { /* разработка */ } @Override public void eat() { /* еда */ } @Override public void sleep() { /* сон */ } @Override public void code() { /* кодирование */ } @Override public void design() { /* не используется */ } @Override public void test() { /* не используется */ } @Override public void deploy() { /* не используется */ } } class Designer implements Worker { @Override public void work() { /* работа */ } @Override public void eat() { /* еда */ } @Override public void sleep() { /* сон */ } @Override public void code() { /* не используется */ } @Override public void design() { /* дизайн */ } @Override public void test() { /* не используется */ } @Override public void deploy() { /* не используется */ } } ``` **Правильная реализация:** ```java // Соблюдение ISP - разделение интерфейсов interface Human { void eat(); void sleep(); } interface Worker extends Human { void work(); } interface Developer extends Worker { void code(); } interface Designer extends Worker { void design(); } interface Tester extends Worker { void test(); } interface DevOps extends Worker { void deploy(); } // Теперь каждый класс реализует только нужные ему интерфейсы class JavaDeveloper implements Developer { @Override public void eat() { /* еда */ } @Override public void sleep() { /* сон */ } @Override public void work() { /* работа */ } @Override public void code() { /* Java кодирование */ } } class UIDesigner implements Designer { @Override public void eat() { /* еда */ } @Override public void sleep() { /* сон */ } @Override public void work() { /* работа */ } @Override public void design() { /* UI дизайн */ } } // Можно комбинировать интерфейсы class FullStackDeveloper implements Developer, Designer { @Override public void eat() { /* еда */ } @Override public void sleep() { /* сон */ } @Override public void work() { /* работа */ } @Override public void code() { /* кодирование */ } @Override public void design() { /* дизайн */ } } ``` ### 5. Dependency Inversion Principle (DIP) **Принцип инверсии зависимостей** > Модули высокого уровня не должны зависеть от модулей низкого уровня **Пример нарушения:** ```java // Нарушение DIP - высокоуровневый модуль зависит от низкоуровневого class EmailService { public void sendEmail(String to, String subject, String body) { // Прямая зависимость от конкретной реализации SMTPClient smtpClient = new SMTPClient(); smtpClient.connect("smtp.gmail.com", 587); smtpClient.send(to, subject, body); smtpClient.disconnect(); } } class OrderService { private EmailService emailService; public OrderService() { this.emailService = new EmailService(); // Жесткая зависимость } public void processOrder(Order order) { // Обработка заказа order.setStatus(OrderStatus.PROCESSED); // Отправка уведомления emailService.sendEmail( order.getCustomerEmail(), "Order Confirmation", "Your order has been processed" ); } } ``` **Правильная реализация:** ```java // Соблюдение DIP - зависимость от абстракций interface NotificationService { void sendNotification(String recipient, String subject, String message); } interface EmailProvider { void sendEmail(String to, String subject, String body); } class SMTPEmailProvider implements EmailProvider { @Override public void sendEmail(String to, String subject, String body) { SMTPClient smtpClient = new SMTPClient(); smtpClient.connect("smtp.gmail.com", 587); smtpClient.send(to, subject, body); smtpClient.disconnect(); } } class SendGridEmailProvider implements EmailProvider { @Override public void sendEmail(String to, String subject, String body) { SendGridClient client = new SendGridClient(); client.send(to, subject, body); } } class EmailNotificationService implements NotificationService { private EmailProvider emailProvider; public EmailNotificationService(EmailProvider emailProvider) { this.emailProvider = emailProvider; // Инъекция зависимости } @Override public void sendNotification(String recipient, String subject, String message) { emailProvider.sendEmail(recipient, subject, message); } } class SMSNotificationService implements NotificationService { private SMSProvider smsProvider; public SMSNotificationService(SMSProvider smsProvider) { this.smsProvider = smsProvider; } @Override public void sendNotification(String recipient, String subject, String message) { smsProvider.sendSMS(recipient, message); } } // Высокоуровневый модуль зависит от абстракции class OrderService { private NotificationService notificationService; public OrderService(NotificationService notificationService) { this.notificationService = notificationService; // Инъекция зависимости } public void processOrder(Order order) { // Обработка заказа order.setStatus(OrderStatus.PROCESSED); // Отправка уведомления через абстракцию notificationService.sendNotification( order.getCustomerEmail(), "Order Confirmation", "Your order has been processed" ); } } // Конфигурация зависимостей @Configuration public class AppConfig { @Bean public EmailProvider emailProvider() { return new SMTPEmailProvider(); // Можно легко заменить на SendGridEmailProvider } @Bean public NotificationService notificationService(EmailProvider emailProvider) { return new EmailNotificationService(emailProvider); } @Bean public OrderService orderService(NotificationService notificationService) { return new OrderService(notificationService); } } ``` ## Дополнительные принципы ### DRY (Не повторяйся) **Не повторяйся** - Избегайте дублирования кода - Выносите общую логику в отдельные функции/классы - Используйте абстракции для переиспользования **Пример нарушения:** ```java // Нарушение DRY - дублирование кода class UserService { public void createUser(String email, String password) { if (email == null || email.isEmpty()) { throw new IllegalArgumentException("Email cannot be null or empty"); } if (!email.contains("@")) { throw new IllegalArgumentException("Invalid email format"); } if (password == null || password.length() < 8) { throw new IllegalArgumentException("Password must be at least 8 characters"); } // создание пользователя } public void updateUser(String email, String password) { if (email == null || email.isEmpty()) { throw new IllegalArgumentException("Email cannot be null or empty"); } if (!email.contains("@")) { throw new IllegalArgumentException("Invalid email format"); } if (password == null || password.length() < 8) { throw new IllegalArgumentException("Password must be at least 8 characters"); } // обновление пользователя } } ``` **Правильная реализация:** ```java // Соблюдение DRY - выделение общей логики class ValidationUtils { public static void validateEmail(String email) { if (email == null || email.isEmpty()) { throw new IllegalArgumentException("Email cannot be null or empty"); } if (!email.contains("@")) { throw new IllegalArgumentException("Invalid email format"); } } public static void validatePassword(String password) { if (password == null || password.length() < 8) { throw new IllegalArgumentException("Password must be at least 8 characters"); } } } class UserService { public void createUser(String email, String password) { ValidationUtils.validateEmail(email); ValidationUtils.validatePassword(password); // создание пользователя } public void updateUser(String email, String password) { ValidationUtils.validateEmail(email); ValidationUtils.validatePassword(password); // обновление пользователя } } ``` ### KISS (Будь проще) **Делай проще** - Простые решения часто лучше сложных - Избегайте преждевременной оптимизации - Читаемость кода важнее "умности" **Пример нарушения:** ```java // Нарушение KISS - излишняя сложность class ComplexCalculator { public double calculate(String expression) { if (expression == null || expression.trim().isEmpty()) { throw new IllegalArgumentException("Expression cannot be null or empty"); } String trimmedExpression = expression.trim(); if (!trimmedExpression.matches("^[0-9+\\-*/\\s()]+$")) { throw new IllegalArgumentException("Invalid characters in expression"); } // Сложная логика парсинга и вычисления... return evaluateComplexExpression(trimmedExpression); } private double evaluateComplexExpression(String expr) { // Очень сложная реализация return 0.0; } } ``` **Правильная реализация:** ```java // Соблюдение KISS - простое решение class SimpleCalculator { public double add(double a, double b) { return a + b; } public double subtract(double a, double b) { return a - b; } public double multiply(double a, double b) { return a * b; } public double divide(double a, double b) { if (b == 0) { throw new IllegalArgumentException("Division by zero"); } return a / b; } } // Использование SimpleCalculator calc = new SimpleCalculator(); double result = calc.add(calc.multiply(2, 3), calc.divide(10, 2)); // 2*3 + 10/2 = 11 ``` ### YAGNI (Вам это не понадобится) **Тебе это не понадобится** - Не реализуйте функциональность "на будущее" - Добавляйте функции только когда они действительно нужны - Избегайте over-engineering **Пример нарушения:** ```java // Нарушение YAGNI - преждевременная оптимизация class UserRepository { private Map cache = new ConcurrentHashMap<>(); private List users = new ArrayList<>(); private UserIndexer indexer = new UserIndexer(); private UserValidator validator = new UserValidator(); private UserMetricsCollector metrics = new UserMetricsCollector(); public void saveUser(User user) { // Валидация (может не понадобиться) validator.validate(user); // Индексация (может не понадобиться) indexer.index(user); // Кэширование (может не понадобиться) cache.put(user.getId(), user); // Сохранение users.add(user); // Метрики (может не понадобиться) metrics.recordSave(user); } } ``` **Правильная реализация:** ```java // Соблюдение YAGNI - только необходимая функциональность class UserRepository { private List users = new ArrayList<>(); public void saveUser(User user) { if (user == null) { throw new IllegalArgumentException("User cannot be null"); } users.add(user); } public User findById(String id) { if (id == null) { throw new IllegalArgumentException("ID cannot be null"); } return users.stream() .filter(u -> u.getId().equals(id)) .findFirst() .orElse(null); } } // Когда понадобится кэширование - добавим // Когда понадобится валидация - добавим // Когда понадобится индексация - добавим ``` ## Принципы модульности ### Слабая связанность (Loose Coupling) - Компоненты минимально зависят друг от друга - Изменения в одном компоненте не влияют на другие - Легче тестировать и поддерживать **Пример нарушения:** ```java // Нарушение - тесная связанность class OrderService { private MySQLDatabase database; private SMTPEmailService emailService; private FileLogger logger; public OrderService() { this.database = new MySQLDatabase("localhost", "orders_db"); this.emailService = new SMTPEmailService("smtp.gmail.com", 587); this.logger = new FileLogger("/var/log/orders.log"); } public void processOrder(Order order) { // Прямая зависимость от конкретных реализаций database.save(order); emailService.sendEmail(order.getCustomerEmail(), "Order processed"); logger.log("Order processed: " + order.getId()); } } ``` **Правильная реализация:** ```java // Соблюдение - слабая связанность interface OrderRepository { void save(Order order); } interface NotificationService { void sendNotification(String recipient, String message); } interface LoggingService { void log(String message); } class OrderService { private OrderRepository repository; private NotificationService notificationService; private LoggingService loggingService; public OrderService(OrderRepository repository, NotificationService notificationService, LoggingService loggingService) { this.repository = repository; this.notificationService = notificationService; this.loggingService = loggingService; } public void processOrder(Order order) { repository.save(order); notificationService.sendNotification( order.getCustomerEmail(), "Order processed" ); loggingService.log("Order processed: " + order.getId()); } } // Конкретные реализации class MySQLOrderRepository implements OrderRepository { @Override public void save(Order order) { // MySQL специфичная логика } } class EmailNotificationService implements NotificationService { @Override public void sendNotification(String recipient, String message) { // Email специфичная логика } } class FileLoggingService implements LoggingService { @Override public void log(String message) { // File logging специфичная логика } } ``` ### Высокая сплоченность (High Cohesion) - Элементы внутри модуля тесно связаны - Модуль имеет четко определенную ответственность - Все функции модуля работают для достижения одной цели **Пример нарушения:** ```java // Нарушение - низкая сплоченность class UtilityClass { public void sendEmail(String to, String subject, String body) { // Отправка email } public void calculateTax(double amount, double rate) { // Расчет налога } public void validatePassword(String password) { // Валидация пароля } public void formatDate(Date date, String format) { // Форматирование даты } public void compressFile(String filePath) { // Сжатие файла } } ``` **Правильная реализация:** ```java // Соблюдение - высокая сплоченность class EmailService { public void sendEmail(String to, String subject, String body) { validateEmail(to); String formattedBody = formatEmailBody(body); deliverEmail(to, subject, formattedBody); } private void validateEmail(String email) { // Валидация email адреса } private String formatEmailBody(String body) { // Форматирование тела письма return body; } private void deliverEmail(String to, String subject, String body) { // Доставка email } } class TaxCalculator { public double calculateTax(double amount, double rate) { validateAmount(amount); validateRate(rate); return amount * rate; } private void validateAmount(double amount) { if (amount < 0) { throw new IllegalArgumentException("Amount cannot be negative"); } } private void validateRate(double rate) { if (rate < 0 || rate > 1) { throw new IllegalArgumentException("Rate must be between 0 and 1"); } } } class PasswordValidator { public boolean isValid(String password) { return hasMinimumLength(password) && containsUppercase(password) && containsLowercase(password) && containsDigit(password); } private boolean hasMinimumLength(String password) { return password.length() >= 8; } private boolean containsUppercase(String password) { return password.chars().anyMatch(Character::isUpperCase); } private boolean containsLowercase(String password) { return password.chars().anyMatch(Character::isLowerCase); } private boolean containsDigit(String password) { return password.chars().anyMatch(Character::isDigit); } } ```