Шаблони проектування: Стратегія (Strategy Pattern)
Ukrainian (українська мова) translation by Anton Bagaiev (you can also view the original English article)
Шаблони проектування: Стратегія(Strategy Pattern)
В цій навчальній серії ми вже охопили три шаблони(або патерни) проектування. Також ми визначили чотири категорії для різних шаблонів проектування. В цій статті я розповім про шаблон стратегії(Strategy Pattern), що підпадає під категорію шаблонів поведінки(Behavioral Design Patterns)
У вас може виникнути питання: коли ми маємо застосовувати цей патерн? І я відповім, що ми маємо застосовувати стратегію, коли у нас є декілька шляхів(алгоритмів) для виконання однієї операції та ми хочемо, щоб програма обирала конкретний шлях відповідно до ваших потреб. Цей патерн також відомий, як патерн поведінки(policy pattern).
Найпростішим прикладом для цього може бути вибір методу сортування. Наприклад, ми маємо декілька алгоритмів сортування масивів, але, залежно від кількості елементів у масиві, ми маємо обрати той алгоритм, що забезпечить найкраще виконання.
Цей патерн також відомий, як патерн поведінки(policy pattern).
Проблема
Для прикладу я візьму інтернет-магазин, у який інтегровано декілька платіжних систем. Крім того, що сайт має декілька платіжних систем, відповідно до вимог, їх не потрібно виводити користувачеві одночасно. Але відповідна платіжна система має бути підключена на льоту, залежно від суми вартості товарів у кошику.
Використовуючи цей приклад, ми можемо визначити наступне: коли сума товарів у кошику менша за 500$, оплату необхідно проводити за допомогою стандарту PayPal, але коли сума є рівною або більша за 500$, нам необхідно використовувати збережені дані кредитної картки(припускаючи, що дані вже збережені).
Без застосування відповідної стратегії наш код буде виглядати так:
Для початку нам треба створити два окремі класи для оплати за допомогою PayPal та оплати за допомогою кредитної карти.
1 |
// Class to pay using Credit Card
|
2 |
class payByCC { |
3 |
|
4 |
private $ccNum = ''; |
5 |
private $ccType = ''; |
6 |
private $cvvNum = ''; |
7 |
private $ccExpMonth = ''; |
8 |
private $ccExpYear = ''; |
9 |
|
10 |
public function pay($amount = 0) { |
11 |
echo "Paying ". $amount. " using Credit Card"; |
12 |
}
|
13 |
|
14 |
}
|
15 |
|
16 |
// Class to pay using PayPal
|
17 |
class payByPayPal { |
18 |
|
19 |
private $payPalEmail = ''; |
20 |
|
21 |
public function pay($amount = 0) { |
22 |
echo "Paying ". $amount. " using PayPal"; |
23 |
}
|
24 |
|
25 |
}
|
26 |
|
27 |
// This code needs to be repeated every place where ever needed.
|
28 |
$amount = 5000; |
29 |
if($amount >= 500) { |
30 |
$pay = new payByCC(); |
31 |
$pay->pay($amount); |
32 |
} else { |
33 |
$pay = new payByPayPal(); |
34 |
$pay->pay($amount); |
35 |
}
|
Також, ви можете зауважити, що нам необхідно додати умови, щоб змусити наш код працювати. Уявіть кількість змін, що ми маємо зробити, якщо нам знадобиться додати зміни у цю логіку, або ми знайдемо помилку, що також змусить нас змінювати код. Тоді нам доведеться патчити всі місця використання цього коду.
Рішення
Тепер ми виконаємо ці вимоги, але з використанням патерну стратегії, що дозволить нам зробити код більш прозорим, зрозумілим та розширюваним.
Інтерфейс
Для цього спочатку введемо інтерфейс, що буде реалізовувати кожен клас платіжної системи. Зрештою, додамо інтерфейси до наших стратегій.
1 |
interface payStrategy { |
2 |
public function pay($amount); |
3 |
}
|
4 |
|
5 |
class payByCC implements payStrategy { |
6 |
|
7 |
|
8 |
private $ccNum = ''; |
9 |
private $ccType = ''; |
10 |
private $cvvNum = ''; |
11 |
private $ccExpMonth = ''; |
12 |
private $ccExpYear = ''; |
13 |
|
14 |
public function pay($amount = 0) { |
15 |
echo "Paying ". $amount. " using Credit Card"; |
16 |
}
|
17 |
|
18 |
}
|
19 |
|
20 |
class payByPayPal implements payStrategy { |
21 |
|
22 |
private $payPalEmail = ''; |
23 |
|
24 |
public function pay($amount = 0) { |
25 |
echo "Paying ". $amount. " using PayPal"; |
26 |
}
|
27 |
|
28 |
}
|
Наступним кроком ми створимо основний клас(у структурі патерну він зазвичай вказується як Context), що зможе використовувати стратегії, які ми вже розробили.
1 |
class shoppingCart { |
2 |
|
3 |
public $amount = 0; |
4 |
|
5 |
public function __construct($amount = 0) { |
6 |
$this->amount = $amount; |
7 |
}
|
8 |
|
9 |
public function getAmount() { |
10 |
return $this->amount; |
11 |
}
|
12 |
|
13 |
public function setAmount($amount = 0) { |
14 |
$this->amount = $amount; |
15 |
}
|
16 |
|
17 |
public function payAmount() { |
18 |
if($this->amount >= 500) { |
19 |
$payment = new payByCC(); |
20 |
} else { |
21 |
$payment = new payByPayPal(); |
22 |
}
|
23 |
|
24 |
$payment->pay($this->amount); |
25 |
|
26 |
}
|
27 |
}
|
Тут ми можемо побачити, що умови вибору потрібної платіжної системи загорнуті у метод payAmount()
. Тепер давайте охопимо весь код і подивимось, як нам використовувати це надалі.
1 |
interface payStrategy { |
2 |
public function pay($amount); |
3 |
}
|
4 |
|
5 |
class payByCC implements payStrategy { |
6 |
|
7 |
private $ccNum = ''; |
8 |
private $ccType = ''; |
9 |
private $cvvNum = ''; |
10 |
private $ccExpMonth = ''; |
11 |
private $ccExpYear = ''; |
12 |
|
13 |
public function pay($amount = 0) { |
14 |
echo "Paying ". $amount. " using Credit Card"; |
15 |
}
|
16 |
|
17 |
}
|
18 |
|
19 |
class payByPayPal implements payStrategy { |
20 |
|
21 |
private $payPalEmail = ''; |
22 |
|
23 |
public function pay($amount = 0) { |
24 |
echo "Paying ". $amount. " using PayPal"; |
25 |
}
|
26 |
|
27 |
}
|
28 |
|
29 |
class shoppingCart { |
30 |
|
31 |
public $amount = 0; |
32 |
|
33 |
public function __construct($amount = 0) { |
34 |
$this->amount = $amount; |
35 |
}
|
36 |
|
37 |
public function getAmount() { |
38 |
return $this->amount; |
39 |
}
|
40 |
|
41 |
public function setAmount($amount = 0) { |
42 |
$this->amount = $amount; |
43 |
}
|
44 |
|
45 |
public function payAmount() { |
46 |
if($this->amount >= 500) { |
47 |
$payment = new payByCC(); |
48 |
} else { |
49 |
$payment = new payByPayPal(); |
50 |
}
|
51 |
|
52 |
$payment->pay($this->amount); |
53 |
}
|
54 |
}
|
55 |
|
56 |
$cart = new shoppingCart(499); |
57 |
$cart->payAmount(); |
58 |
|
59 |
// Output
|
60 |
Paying 499 using PayPal |
61 |
|
62 |
$cart = new shoppingCart(501); |
63 |
$cart->payAmount(); |
64 |
|
65 |
//Output
|
66 |
Paying 501 using Credit Card |
Ми можемо побачити, що вибір платіжної системи недоступний для нашої програми. Також, згідно вимог, ми маємо реалізацію всіх відповідних платіжних систем для проведення банківської транзакції.
Додаємо нову стратегію
Якщо пізніше нам знадобиться додати нову стратегію(в цьому прикладі - нову платіжну систему) з іншою логікою, це буде дуже легко зробити у нашому випадку. Скажімо, ми хочемо додати новую платіжну систему Moneybooker, і хочемо використовувати її, коли маємо вартість товарів у кошику більше за 500$, але менше за 1000$.
Все, що ми маємо зробити - це додати новий клас стратегії, що реалізує вищезазначений інтерфейс, і ми можемо просуватись далі.
1 |
class payByMB implements payStrategy { |
2 |
|
3 |
private $mbEmail = ''; |
4 |
|
5 |
public function pay($amount = 0) { |
6 |
echo "Paying ". $amount. " using Money Booker"; |
7 |
}
|
8 |
|
9 |
}
|
Тепер ми маємо наш новий клас стратегії, і все, що нам залишилось змінити, це наш основний метод payAmount()
. Він потребує таких змін:
1 |
public function payAmount() { |
2 |
|
3 |
if($this->amount > 500 && $this->amount < 1000) { |
4 |
$payment = new payByMB(); |
5 |
} else if($this->amount >= 500) { |
6 |
$payment = new payByCC(); |
7 |
} else { |
8 |
$payment = new payByPayPal(); |
9 |
}
|
10 |
|
11 |
$payment->pay($this->amount); |
12 |
}
|
Ви можете побачити, що ми зробили зміни лише в методі payAmount()
, замість зміни коду, що викликає цей метод.
Отже
Ми можемо зробити висновок, що, маючи декілька шляхів виконання схожих завдань(в прикладному програмуванні радше говорити - коли ми маємо декілька алгоритмів для виконання однієї операції), ми маємо прибігати до використання шаблону стратегії.
Використовуючи цей патерн, нам легко додавати чи видаляти алгоритми, тому що зміна цих алгоритмів недоступна основній програмі.
Я спробував максимально зрозуміло використати простий, але практичний приклад для демонстрації шаблону стратегії, тож, якщо у вас залишились коментарі чи питання, не соромтесь задавати їх у коментарях до статті.