Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Паттерн Strategy: мой опыт реализации
Strategy - это поведенческий паттерн проектирования, который позволяет определить семейство алгоритмов, инкапсулировать каждый и сделать их взаимозаменяемыми. Я использовал его в нескольких реальных проектах.
Что такое Strategy?
Strategy паттерн решает проблему: когда у нас есть несколько способов сделать одно и то же, но выбор способа зависит от контекста.
Без паттерна (плохо)
public class PaymentProcessor {
public void processPayment(Order order, String paymentMethod) {
if (paymentMethod.equals("credit_card")) {
// Код для кредитной карты
validateCreditCard();
chargeCreditCard();
logCreditCard();
} else if (paymentMethod.equals("paypal")) {
// Код для PayPal
validatePayPalAccount();
chargePayPal();
logPayPal();
} else if (paymentMethod.equals("crypto")) {
// Код для крипто
validateWallet();
chargeCrypto();
logCrypto();
}
}
}
// Проблемы:
// ❌ Огромный метод со множеством if/else
// ❌ Сложно добавлять новые методы оплаты
// ❌ Логика разных методов смешана
// ❌ Нарушает SRP и OCP (SOLID)
Решение с Strategy паттерном
Шаг 1: Определяем Strategy интерфейс
// Интерфейс, который определяет контракт для всех стратегий
public interface PaymentStrategy {
void validate(Order order) throws ValidationException;
void charge(Order order) throws PaymentException;
void log(Order order, PaymentResult result);
}
Шаг 2: Реализуем конкретные стратегии
// Стратегия для кредитной карты
public class CreditCardPaymentStrategy implements PaymentStrategy {
private CreditCardValidator validator;
private CreditCardGateway gateway;
private PaymentLogger logger;
public CreditCardPaymentStrategy(CreditCardValidator validator,
CreditCardGateway gateway,
PaymentLogger logger) {
this.validator = validator;
this.gateway = gateway;
this.logger = logger;
}
@Override
public void validate(Order order) throws ValidationException {
Card card = order.getPaymentInfo().getCard();
validator.validateCardNumber(card.getNumber());
validator.validateExpiryDate(card.getExpiryDate());
validator.validateCVV(card.getCvv());
}
@Override
public void charge(Order order) throws PaymentException {
Card card = order.getPaymentInfo().getCard();
gateway.authorizePayment(order.getTotal(), card);
gateway.capturePayment(order.getOrderId());
}
@Override
public void log(Order order, PaymentResult result) {
logger.log("Credit Card Payment", order.getOrderId(), result.getStatus());
}
}
// Стратегия для PayPal
public class PayPalPaymentStrategy implements PaymentStrategy {
private PayPalValidator validator;
private PayPalAPI payPalAPI;
private PaymentLogger logger;
public PayPalPaymentStrategy(PayPalValidator validator,
PayPalAPI payPalAPI,
PaymentLogger logger) {
this.validator = validator;
this.payPalAPI = payPalAPI;
this.logger = logger;
}
@Override
public void validate(Order order) throws ValidationException {
String email = order.getPaymentInfo().getPayPalEmail();
validator.validateEmail(email);
validator.validateAccountExists(email);
}
@Override
public void charge(Order order) throws PaymentException {
String email = order.getPaymentInfo().getPayPalEmail();
payPalAPI.requestPayment(email, order.getTotal(), order.getOrderId());
}
@Override
public void log(Order order, PaymentResult result) {
logger.log("PayPal Payment", order.getOrderId(), result.getStatus());
}
}
// Стратегия для крипто
public class CryptoPaymentStrategy implements PaymentStrategy {
private CryptoValidator validator;
private BlockchainAPI blockchainAPI;
private PaymentLogger logger;
public CryptoPaymentStrategy(CryptoValidator validator,
BlockchainAPI blockchainAPI,
PaymentLogger logger) {
this.validator = validator;
this.blockchainAPI = blockchainAPI;
this.logger = logger;
}
@Override
public void validate(Order order) throws ValidationException {
String wallet = order.getPaymentInfo().getWalletAddress();
validator.validateWalletAddress(wallet);
validator.validateSufficientBalance(wallet, order.getTotal());
}
@Override
public void charge(Order order) throws PaymentException {
String wallet = order.getPaymentInfo().getWalletAddress();
String txHash = blockchainAPI.transferFunds(
wallet,
order.getTotal(),
order.getOrderId()
);
blockchainAPI.waitForConfirmation(txHash);
}
@Override
public void log(Order order, PaymentResult result) {
logger.log("Crypto Payment", order.getOrderId(), result.getStatus());
}
}
Шаг 3: Контекст, который использует стратегию
@Service
public class PaymentProcessor {
private Map<String, PaymentStrategy> strategies = new HashMap<>();
// Dependency Injection
public PaymentProcessor(CreditCardPaymentStrategy ccStrategy,
PayPalPaymentStrategy ppStrategy,
CryptoPaymentStrategy cryptoStrategy) {
strategies.put("credit_card", ccStrategy);
strategies.put("paypal", ppStrategy);
strategies.put("crypto", cryptoStrategy);
}
// Альтернатива с Factory
public void registerStrategy(String type, PaymentStrategy strategy) {
strategies.put(type, strategy);
}
// Главный метод обработки платежа
public PaymentResult processPayment(Order order, String paymentMethodType)
throws PaymentException {
// Получаем правильную стратегию
PaymentStrategy strategy = strategies.get(paymentMethodType);
if (strategy == null) {
throw new UnknownPaymentMethodException(
"Unknown payment method: " + paymentMethodType
);
}
try {
// [1] Валидация
strategy.validate(order);
// [2] Обработка платежа
strategy.charge(order);
// [3] Логирование
PaymentResult result = new PaymentResult(
order.getOrderId(),
PaymentStatus.SUCCESS,
paymentMethodType
);
strategy.log(order, result);
return result;
} catch (ValidationException e) {
throw new PaymentException("Payment validation failed: " + e.getMessage(), e);
} catch (PaymentException e) {
throw e;
}
}
}
Использование в контроллере
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final PaymentProcessor paymentProcessor;
@PostMapping("/{orderId}/pay")
public ResponseEntity<PaymentResponse> payOrder(
@PathVariable Long orderId,
@RequestBody PaymentRequest request) {
try {
// Получаем заказ
Order order = orderService.getOrder(orderId);
// Обрабатываем платёж (стратегия выбирается автоматически)
PaymentResult result = paymentProcessor.processPayment(
order,
request.getPaymentMethod() // "credit_card", "paypal", "crypto"
);
// Обновляем заказ
order.markAsPaid(result);
orderService.save(order);
return ResponseEntity.ok(new PaymentResponse(
result.getOrderId(),
result.getStatus()
));
} catch (PaymentException e) {
return ResponseEntity.status(400)
.body(new PaymentResponse(orderId, PaymentStatus.FAILED));
}
}
}
Конфигурация Spring для DI
@Configuration
public class PaymentConfig {
@Bean
public CreditCardValidator creditCardValidator() {
return new CreditCardValidator();
}
@Bean
public CreditCardGateway creditCardGateway() {
return new StripeGateway(); // Например, Stripe
}
@Bean
public PaymentLogger paymentLogger() {
return new DatabasePaymentLogger();
}
// Регистрируем стратегии как бины
@Bean
public CreditCardPaymentStrategy creditCardPaymentStrategy(
CreditCardValidator validator,
CreditCardGateway gateway,
PaymentLogger logger) {
return new CreditCardPaymentStrategy(validator, gateway, logger);
}
@Bean
public PayPalPaymentStrategy payPalPaymentStrategy(
PayPalValidator validator,
PayPalAPI api,
PaymentLogger logger) {
return new PayPalPaymentStrategy(validator, api, logger);
}
@Bean
public CryptoPaymentStrategy cryptoPaymentStrategy(
CryptoValidator validator,
BlockchainAPI api,
PaymentLogger logger) {
return new CryptoPaymentStrategy(validator, api, logger);
}
@Bean
public PaymentProcessor paymentProcessor(
CreditCardPaymentStrategy cc,
PayPalPaymentStrategy pp,
CryptoPaymentStrategy crypto) {
return new PaymentProcessor(cc, pp, crypto);
}
}
Преимущества решения
// ✅ OCP (Open/Closed Principle)
// Класс открыт для расширения (можем добавить новую стратегию)
// Закрыт для модификации (не меняем PaymentProcessor)
// ✅ SRP (Single Responsibility)
// Каждая стратегия отвечает только за один метод оплаты
// ✅ DIP (Dependency Inversion)
// PaymentProcessor зависит от интерфейса, а не от конкретных реализаций
// ✅ Легко тестировать
public class PaymentProcessorTest {
@Test
void testCreditCardPayment() {
// Mock стратегию
PaymentStrategy mockStrategy = mock(PaymentStrategy.class);
PaymentProcessor processor = new PaymentProcessor(mockStrategy, null, null);
// Тестируем
}
}
Добавление новой стратегии
Чтобы добавить новый метод оплаты (например, Apple Pay):
// 1. Создаём новую стратегию
public class ApplePayStrategy implements PaymentStrategy {
// Реализуем методы
}
// 2. Регистрируем в конфигурации
@Bean
public ApplePayStrategy applePayStrategy(...) {
return new ApplePayStrategy(...);
}
// 3. Добавляем в PaymentProcessor
// (если используем список)
paymentProcessor.registerStrategy("apple_pay", applePayStrategy);
// 4. Всё работает!
// Никаких изменений в основном коде PaymentProcessor
Реальный пример из моего проекта
В одном проекте я использовал Strategy для разных алгоритмов сортировки результатов поиска:
public interface SearchRankingStrategy {
List<SearchResult> rank(List<SearchResult> results, SearchQuery query);
}
// Стратегия 1: По релевантности (TF-IDF)
public class RelevanceRankingStrategy implements SearchRankingStrategy { ... }
// Стратегия 2: По популярности
public class PopularityRankingStrategy implements SearchRankingStrategy { ... }
// Стратегия 3: По дате
public class RecencyRankingStrategy implements SearchRankingStrategy { ... }
// Используем в сервисе
@Service
public class SearchService {
public List<SearchResult> search(String query, String rankingType) {
SearchRankingStrategy strategy = strategies.get(rankingType);
List<SearchResult> results = elasticSearch.search(query);
return strategy.rank(results, query);
}
}
Когда использовать Strategy
✓ Несколько способов выполнить задачу
✓ Алгоритм может меняться в runtime
✓ Хотим избежать больших if/else/switch блоков
✓ Каждый алгоритм независим
✓ Нужна возможность добавлять новые алгоритмы
✗ Только один способ сделать что-то
✗ Алгоритмы редко меняются
✗ Много общего кода между алгоритмами (лучше Template Method)
Резюме
Strategy паттерн - один из самых полезных паттернов, который я использую регулярно:
- Определяем интерфейс с контрактом алгоритма
- Создаём конкретные реализации для каждого случая
- Используем Map/Factory для выбора нужной стратегии
- Контекст работает с интерфейсом, а не с конкретными классами
Это делает код чище, более maintainable и соответствует SOLID принципам. Я нашёл это особенно полезным в платёжных системах, системах рекомендаций и при работе с разными источниками данных.