Типовий приклад — ви приймаєте кілька способів оплати: кредитну картку, PayPal та криптовалюту. І в залежності від вибору користувача, ви будете обробляти платіж по-різному.
Наприклад, якщо користувач вибрав PayPal, вам потрібно працювати з API PayPal для обробки платежу.
Якщо ви прийшли з мови програмування, що підтримує об'єктно-орієнтоване програмування (OOP), найкращий спосіб реалізувати це — використовуючи Стратегію.
Спосіб з OOP
Ось як виглядатиме версія з використанням OOP:
interface IPayment {
processPayment(amount: number): void
}
class PaymentProcessor implements IPayment {
private paymentStrategy: IPayment
constructor(strategy: IPayment) {
this.paymentStrategy = strategy
}
processPayment(amount: number): void {
this.paymentStrategy.processPayment(amount)
}
setPaymentStrategy(strategy: IPayment) {
this.paymentStrategy = strategy
}
}
class CreditCardPayment implements IPayment {
processPayment(amount: number): void {
console.log(`$${amount} було оброблено через кредитну картку`)
}
}
class PayPalPayment implements IPayment {
processPayment(amount: number): void {
console.log(`$${amount} було оброблено через PayPal`)
}
}
class CryptoPayment implements IPayment {
processPayment(amount: number): void {
console.log(`$${amount} було оброблено через криптовалюту`)
}
}
// Використання
const paymentProcessor = new PaymentProcessor(new PayPalPayment())
paymentProcessor.processPayment(100)
// Вивід: $100 було оброблено через PayPal
Досить багато коду для одного методу.
Дуже багатослівно!
Якщо ви працюєте з мовою, яка суворо підтримує об'єктно-орієнтоване програмування (OOP), наприклад, Java, це може бути ваш єдиний варіант.
Але якщо ваша мова підтримує функції як повноцінні сутності, як це робить JavaScript, використання функціонального підходу буде набагато чистішим.
Функціональний підхід
Ось як це виглядатиме з функціями:
type PaymentStrategy = (amount: number) => void
function processPayment(strategy: PaymentStrategy, amount: number) {
strategy(amount)
}
const creditCardPayment: PaymentStrategy = (amount: number) => {
console.log(`$${amount} було оброблено за допомогою кредитної картки`)
}
const paypalPayment: PaymentStrategy = (amount: number) => {
console.log(`$${amount} було оброблено за допомогою PayPal`)
}
const cryptoPayment: PaymentStrategy = (amount: number) => {
console.log(`$${amount} було оброблено за допомогою криптовалюти`)
}
processPayment(paypalPayment, 100)
// Вивід: $100 було оброблено за допомогою PayPal
Жодного зайвого коду і значно чистіше!
Оскільки функції є значно меншими одиницями, ніж класи, їх набагато простіше повторно використовувати, замінювати або замокати.
Вам не потрібно створювати екземпляр класу та налаштовувати залежності; ви просто працюєте з однією функцією.
З функціями ви передаєте лише те, що вам потрібно
В ООП ми використовуємо ISP (Принцип сегрегації інтерфейсів), щоб гарантувати, що класи не залежать від методів, якими вони не користуються.
Використовуючи той самий приклад, ось як буде виглядати ваш код без застосування ISP.
interface IPayment {
processPayment(amount: number): void
}
interface PaymentStrategy {
processPayment(amount: number): void
email(): string
creditCardNumber(): string
publicKey(): string
privateKey(): string
}
class PaymentProcessor implements IPayment {
// так само
}
class CreditCardPayment implements PaymentStrategy {
private _creditCardNumber: string
constructor(creditCardNumber: string) {
this._creditCardNumber = creditCardNumber
}
processPayment(amount: number): void {
console.log(
`$${amount} було оброблено за допомогою кредитної картки, використовуючи номер картки ${this.creditCardNumber()}`
)
}
creditCardNumber(): string {
return this._creditCardNumber
}
email(): string {
throw new Error('Метод не реалізовано.')
}
publicKey(): string {
throw new Error('Метод не реалізовано.')
}
privateKey(): string {
throw new Error('Метод не реалізовано.')
}
}
class PayPalPayment implements PaymentStrategy {
private _email: string
constructor(email: string) {
this._email = email
}
processPayment(amount: number): void {
console.log(
`$${amount} було оброблено за допомогою PayPal, використовуючи email ${this.email()}`
)
}
email(): string {
return this._email
}
creditCardNumber(): string {
throw new Error('Метод не реалізовано.')
}
publicKey(): string {
throw new Error('Метод не реалізовано.')
}
privateKey(): string {
throw new Error('Метод не реалізовано.')
}
}
class CryptoPayment implements PaymentStrategy {
// ...
}
// Використання
const paymentProcessor = new PaymentProcessor(new CreditCardPayment('1234'))
paymentProcessor.processPayment(100)
const paymentProcessor2 = new PaymentProcessor(
new PayPalPayment('[email protected]')
)
paymentProcessor2.processPayment(100)
const paymentProcessor3 = new PaymentProcessor(
new CryptoPayment('public_key', 'private_key')
)
paymentProcessor3.processPayment(100)
У цьому коді ми маємо один інтерфейс для всіх платіжних стратегій. Але не всі методи використовуються.
Наприклад, для PayPal нам потрібен лише email, решта не потрібна.
Щоб виправити це, ми можемо застосувати ISP (Принцип розділення інтерфейсів) і розділити інтерфейс на кілька менших інтерфейсів, кожен з яких відповідає за конкретний метод оплати.
Але навіщо це робити, якщо ми можемо уникнути всього цього за допомогою функціонального підходу.
type PaymentStrategy = (amount: number) => void
function processPayment(strategy: PaymentStrategy, amount: number) {
strategy(amount)
}
const creditCardPayment = (creditCardNumber: string): PaymentStrategy => {
return (amount: number) => {
console.log(
`$${amount} був оброблений за допомогою кредитної картки з номером ${creditCardNumber}`
)
}
}
const paypalPayment = (email: string): PaymentStrategy => {
return (amount: number) => {
console.log(`$${amount} був оброблений через PayPal за допомогою email ${email}`)
}
}
const cryptoPayment = (publicKey: string, privateKey: string): PaymentStrategy => {
return (amount: number) => {
console.log(
`$${amount} був оброблений через Crypto з публічним ключем ${publicKey} та приватним ключем ${privateKey}`
)
}
}
// Використання
processPayment(creditCardPayment('1234'), 100)
processPayment(paypalPayment('[email protected]'), 100)
processPayment(cryptoPayment('publicKey', 'privateKey'), 100)
Завдяки можливостям замикань та часткового застосування ми перетворили кожну функцію оплати на функцію з одним параметром після того, як вказали необхідні деталі (наприклад, email для PayPal).
Наприклад, виклик paypalPayment('test@example')
повертає іншу функцію, яка приймає суму: (amount: number) => void
.
Тепер передача цієї другої функції в функцію processPayment
буде працювати, тому що саме це вона і очікує.
Ось чому функції краще підходять для патерну Стратегії
Це менше шаблонного коду, простіше тестувати та мокати, і це запобігає надмірним інтерфейсам, надаючи лише необхідне (як ми побачили в останньому прикладі).
🔗 Залишаймося на зв'язку:
- 🌐 Блог: https://tahazsh.com/
- 𝕏 Twitter/X: https://twitter.com/tahazsh
- 🦋 Bluesky: https://bsky.app/profile/tahazsh.bsky.social
- 🐘 Mastodon: https://fosstodon.org/@tahazsh
- 🎥 YouTube: https://www.youtube.com/@tahazsh
Перекладено з: Strategy pattern is better when done with functions