Дизайнерські патерни — потужний спосіб створювати гнучке, багаторазове та легко підтримуване програмне забезпечення. Патерн Стратегії (Strategy Pattern) є патерном поведінки, який дозволяє визначити родину алгоритмів, інкапсулювати їх та зробити їх взаємозамінними під час виконання. Цей патерн надзвичайно корисний, коли ви хочете визначити різні способи виконання певної операції та динамічно перемикатися між ними без зміни контексту, в якому вони використовуються.
У цій статті ми детально розглянемо Патерн Стратегії, наводячи покрокові приклади на Java, які демонструють, як використовувати цей патерн у реальних сценаріях.
Що таке Патерн Стратегії?
Патерн Стратегії визначає набір алгоритмів (стратегій), які можуть бути замінені під час виконання. Замість того, щоб жорстко прописувати логіку в клієнтському коді, клієнт залежить від інтерфейсу, а різні стратегії реалізують цей інтерфейс. Це сприяє дотриманню принципу відкритості/закритості — система відкрита для розширення, але закрита для модифікацій.
Основні компоненти Патерну Стратегії:
- Стратегія (інтерфейс): Визначає спільний інтерфейс для всіх конкретних стратегій.
- Конкретна Стратегія: Реалізує інтерфейс стратегії з конкретним алгоритмом.
- Контекст: Утримує посилання на об'єкт стратегії і делегує йому виконання роботи.
Коли використовувати Патерн Стратегії:
- Коли потрібно кілька варіацій одного алгоритму.
- Коли хочеться уникнути великих умовних операторів if-else або switch-case.
- Коли поведінка класу повинна бути легко розширюваною без зміни клієнтського коду.
- Коли кілька класів мають спільну логіку, але відрізняються алгоритмами.
1. Приклад: Система обробки платежів
Створимо систему обробки платежів, де користувачі можуть обирати між різними методами оплати (кредитна картка, PayPal або криптовалюта).
Крок 1: Визначаємо інтерфейс стратегії
Інтерфейс PaymentStrategy визначає контракт для різних стратегій оплати.
interface PaymentStrategy {
void pay(double amount);
}
Крок 2: Реалізуємо конкретні стратегії
Тут ми визначаємо три різні стратегії оплати.
1. Стратегія оплати карткою
class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(double amount) {
System.out.println("Заплачено $" + amount + " за допомогою картки: " + cardNumber);
}
}
- Стратегія оплати через PayPal
class PayPalPayment implements PaymentStrategy {
private String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(double amount) {
System.out.println("Заплачено $" + amount + " через аккаунт PayPal: " + email);
}
}
Стратегія оплати криптовалютою
class CryptoPayment implements PaymentStrategy {
private String walletAddress;
public CryptoPayment(String walletAddress) {
this.walletAddress = walletAddress;
}
@Override
public void pay(double amount) {
System.out.println("Заплачено $" + amount + " за допомогою криптовалютного гаманця: " + walletAddress);
}
}
```
Крок 3: Створення класу Контексту
Клас PaymentContext утримує посилання на стратегію оплати і делегує операцію оплати вибраній стратегії.
class PaymentContext {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void pay(double amount) {
if (paymentStrategy == null) {
throw new IllegalStateException("Стратегію оплати не встановлено");
}
paymentStrategy.pay(amount);
}
}
Крок 4: Клієнтський код
Клієнт може динамічно обирати стратегію оплати під час виконання програми.
public class StrategyPatternDemo {
public static void main(String[] args) {
PaymentContext paymentContext = new PaymentContext();
System.out.println("Вибір оплати карткою:");
paymentContext.setPaymentStrategy(new CreditCardPayment("1234-5678-9876-5432"));
paymentContext.pay(150.75);
System.out.println("\nВибір оплати через PayPal:");
paymentContext.setPaymentStrategy(new PayPalPayment("[email protected]"));
paymentContext.pay(200.00);
System.out.println("\nВибір оплати криптовалютою:");
paymentContext.setPaymentStrategy(new CryptoPayment("wallet123xyz"));
paymentContext.pay(350.00);
}
}
Вихід:
Вибір оплати карткою:
Заплачено $150.75 за допомогою картки: 1234-5678-9876-5432
Вибір оплати через PayPal:
Заплачено $200.0 через аккаунт PayPal: [email protected]
Вибір оплати криптовалютою:
Заплачено $350.0 за допомогою криптовалютного гаманця: wallet123xyz
У цьому прикладі клієнт може змінювати стратегії оплати без змін в основному коді. Клас PaymentContext делегує логіку оплати вибраній стратегії під час виконання програми.
2. Реальний приклад: Алгоритми сортування
Ще одним поширеним випадком використання Патерну Стратегії є вибір алгоритмів сортування під час виконання програми.
Крок 1: Визначення інтерфейсу стратегії сортування
interface SortingStrategy {
void sort(int[] numbers);
}
Крок 2: Реалізація конкретних стратегій сортування
1. Сортування бульбашковим методом
class BubbleSort implements SortingStrategy {
@Override
public void sort(int[] numbers) {
int n = numbers.length;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (numbers[j] > numbers[j + 1]) {
int temp = numbers[j];
numbers[j] = numbers[j + 1];
numbers[j + 1] = temp;
}
}
}
System.out.println("Відсортовано за допомогою бульбашкового сортування: " + java.util.Arrays.toString(numbers));
}
}
Стратегія оплати криптовалютою
class CryptoPayment implements PaymentStrategy {
private String walletAddress;
public CryptoPayment(String walletAddress) {
this.walletAddress = walletAddress;
}
@Override
public void pay(double amount) {
System.out.println("Заплачено $" + amount + " за допомогою криптовалютного гаманця: " + walletAddress);
}
}
```
Крок 3: Створення класу Контексту
Клас PaymentContext утримує посилання на стратегію оплати і делегує операцію оплати вибраній стратегії.
class PaymentContext {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void pay(double amount) {
if (paymentStrategy == null) {
throw new IllegalStateException("Стратегію оплати не встановлено");
}
paymentStrategy.pay(amount);
}
}
Крок 4: Клієнтський код
Клієнт може динамічно обирати стратегію оплати під час виконання програми.
public class StrategyPatternDemo {
public static void main(String[] args) {
PaymentContext paymentContext = new PaymentContext();
System.out.println("Вибір оплати карткою:");
paymentContext.setPaymentStrategy(new CreditCardPayment("1234-5678-9876-5432"));
paymentContext.pay(150.75);
System.out.println("\nВибір оплати через PayPal:");
paymentContext.setPaymentStrategy(new PayPalPayment("[email protected]"));
paymentContext.pay(200.00);
System.out.println("\nВибір оплати криптовалютою:");
paymentContext.setPaymentStrategy(new CryptoPayment("wallet123xyz"));
paymentContext.pay(350.00);
}
}
Вихід:
Вибір оплати карткою:
Заплачено $150.75 за допомогою картки: 1234-5678-9876-5432
Вибір оплати через PayPal:
Заплачено $200.0 через аккаунт PayPal: [email protected]
Вибір оплати криптовалютою:
Заплачено $350.0 за допомогою криптовалютного гаманця: wallet123xyz
У цьому прикладі клієнт може змінювати стратегії оплати без змін в основному коді. Клас PaymentContext делегує логіку оплати вибраній стратегії під час виконання програми.
2. Реальний приклад: Алгоритми сортування
Ще одним поширеним випадком використання Патерну Стратегії є вибір алгоритмів сортування під час виконання програми.
Крок 1: Визначення інтерфейсу стратегії сортування
interface SortingStrategy {
void sort(int[] numbers);
}
Крок 2: Реалізація конкретних стратегій сортування
1. Сортування бульбашковим методом
class BubbleSort implements SortingStrategy {
@Override
public void sort(int[] numbers) {
int n = numbers.length;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (numbers[j] > numbers[j + 1]) {
int temp = numbers[j];
numbers[j] = numbers[j + 1];
numbers[j + 1] = temp;
}
}
}
System.out.println("Відсортовано за допомогою бульбашкового сортування: " + java.util.Arrays.toString(numbers));
}
}
2. Швидке сортування
class QuickSort implements SortingStrategy {
@Override
public void sort(int[] numbers) {
quickSort(numbers, 0, numbers.length - 1);
System.out.println("Відсортовано за допомогою швидкого сортування: " + java.util.Arrays.toString(numbers));
}
private void quickSort(int[] array, int low, int high) {
if (low < high) {
int pivot = partition(array, low, high);
quickSort(array, low, pivot - 1);
quickSort(array, pivot + 1, high);
}
}
private int partition(int[] array, int low, int high) {
int pivot = array[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (array[j] < pivot) {
i++;
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
int temp = array[i + 1];
array[i + 1] = array[high];
array[high] = temp;
return i + 1;
}
}
Крок 3: Створення класу Контексту
class SortContext {
private SortingStrategy sortingStrategy;
public void setSortingStrategy(SortingStrategy sortingStrategy) {
this.sortingStrategy = sortingStrategy;
}
public void sortArray(int[] numbers) {
if (sortingStrategy == null) {
throw new IllegalStateException("Стратегію сортування не встановлено");
}
sortingStrategy.sort(numbers);
}
}
Крок 4: Клієнтський код
public class SortingStrategyDemo {
public static void main(String[] args) {
SortContext sortContext = new SortContext();
int[] numbers = {5, 2, 9, 1, 6};
System.out.println("Використовуємо бульбашкове сортування:");
sortContext.setSortingStrategy(new BubbleSort());
sortContext.sortArray(numbers.clone());
System.out.println("\nВикористовуємо швидке сортування:");
sortContext.setSortingStrategy(new QuickSort());
sortContext.sortArray(numbers.clone());
}
}
Вихід:
Використовуємо бульбашкове сортування:
Відсортовано за допомогою бульбашкового сортування: [1, 2, 5, 6, 9]
Використовуємо швидке сортування:
Відсортовано за допомогою швидкого сортування: [1, 2, 5, 6, 9]
У цьому прикладі стратегія сортування задається динамічно під час виконання програми, залежно від вибору користувача.
Переваги Патерну Стратегії:
- Принцип відкритості/закритості (Open/Closed Principle): Нові стратегії можна додавати без змін існуючого коду.
- Уникнення умовних конструкцій: Уникається використання довгих конструкцій if-else або switch.
- Змінюваність поведінки: Легко перемикатися між різними алгоритмами під час виконання програми.
- Інкапсуляція: Кожен алгоритм зберігається в окремому класі, що покращує читаємість коду.
Недоліки Патерну Стратегії:
- Збільшена кількість класів: Кожна стратегія потребує нового класу, що може призвести до великої кількості класів у програмі.
- Конфігурація Контексту: Потрібна правильна ініціалізація та управління контекстом.
Висновок:
Патерн Стратегії є потужним інструментом для розробки гнучких і підтримуваних Java-додатків. Інкапсульовані алгоритми, які можна легко змінювати, надають чистий спосіб вибору поведінки під час виконання програми без необхідності заплутувати код складними умовними конструкціями. Чи то розробка платіжної системи, чи вибір алгоритму сортування, чи будь-яка інша операція, яка може варіюватися залежно від вимог, Патерн Стратегії забезпечує дотримання принципів гарного дизайну програмного забезпечення.
Перекладено з: Mastering the Strategy Design Pattern in Java: A Comprehensive Guide with Examples