Приведите пример шаблона проектирования "Стратегия" и объясните, в каких ситуациях его применение предпочтительнее наследования; покажите псевдокод или пример на любом языке
Кратко — что такое «Стратегия»: это поведенческий шаблон, который инкапсулирует набор взаимозаменяемых алгоритмов (стратегий) в отдельные классы и позволяет менять алгоритм выполнения в рантайме, не изменяя клиента (контекста). Когда предпочтительнее использовать стратегию, а не наследование: - Когда алгоритмы/политики меняются независимо от класса клиента и их нужно выбирать или менять во время выполнения. - Чтобы избежать взрыва подклассов (комбинации разных поведений лучше задавать композиционно, а не через все возможные подклассы). - Для соблюдения принципа «композиция вместо наследования» и SRP (каждый класс — одна ответственность). - Для упрощения тестирования (стратегии можно тестировать отдельно и подменять заглушками). Когда наследование может быть достаточным: поведение фиксировано, не нужно менять в рантайме и дизайн прост; наследование проще реализовать и понятнее для очень узких случаев. Пример (Java-подобный, можно легко перевести в любой язык). Сценарий: оплата — разные способы оплаты как стратегии. interface PaymentStrategy { void pay(double amount); } class CreditCardPayment implements PaymentStrategy { String cardNumber; CreditCardPayment(String cardNumber) { this.cardNumber = cardNumber; } @Override void pay(double amount) { // логика оплаты картой System.out.println("Paying " + amount + " by credit card " + cardNumber); } } class PayPalPayment implements PaymentStrategy { String email; PayPalPayment(String email) { this.email = email; } @Override void pay(double amount) { // логика оплаты через PayPal System.out.println("Paying " + amount + " via PayPal account " + email); } } class CheckoutContext { private PaymentStrategy strategy; CheckoutContext(PaymentStrategy initial) { this.strategy = initial; } void setStrategy(PaymentStrategy s) { this.strategy = s; } // смена в рантайме void payOrder(double amount) { strategy.pay(amount); // делегирование стратегии } } Пример использования: PaymentStrategy s1 = new CreditCardPayment("1234-****-****-5678"); CheckoutContext ctx = new CheckoutContext(s1); ctx.payOrder(99.99); // платим картой ctx.setStrategy(new PayPalPayment("user@example.com")); ctx.payOrder(49.50); // теперь платим через PayPal Пояснения к примеру: - PaymentStrategy — интерфейс (контракт) для алгоритмов оплаты. - Конкретные стратегии инкапсулируют разные реализации оплаты. - CheckoutContext не знает деталей реализации, он просто делегирует вызов стратегии. - Мы можем менять стратегию в рантайме методом setStrategy, что было бы неудобно или невозможно при чистом наследовании (требовало бы создавать новые подклассы для каждой комбинации поведения). Это иллюстрирует основные преимущества шаблона «Стратегия» перед наследованием в описанных ситуациях.
Когда предпочтительнее использовать стратегию, а не наследование:
- Когда алгоритмы/политики меняются независимо от класса клиента и их нужно выбирать или менять во время выполнения.
- Чтобы избежать взрыва подклассов (комбинации разных поведений лучше задавать композиционно, а не через все возможные подклассы).
- Для соблюдения принципа «композиция вместо наследования» и SRP (каждый класс — одна ответственность).
- Для упрощения тестирования (стратегии можно тестировать отдельно и подменять заглушками).
Когда наследование может быть достаточным: поведение фиксировано, не нужно менять в рантайме и дизайн прост; наследование проще реализовать и понятнее для очень узких случаев.
Пример (Java-подобный, можно легко перевести в любой язык). Сценарий: оплата — разные способы оплаты как стратегии.
interface PaymentStrategy {
void pay(double amount);
}
class CreditCardPayment implements PaymentStrategy {
String cardNumber;
CreditCardPayment(String cardNumber) { this.cardNumber = cardNumber; }
@Override
void pay(double amount) {
// логика оплаты картой
System.out.println("Paying " + amount + " by credit card " + cardNumber);
}
}
class PayPalPayment implements PaymentStrategy {
String email;
PayPalPayment(String email) { this.email = email; }
@Override
void pay(double amount) {
// логика оплаты через PayPal
System.out.println("Paying " + amount + " via PayPal account " + email);
}
}
class CheckoutContext {
private PaymentStrategy strategy;
CheckoutContext(PaymentStrategy initial) { this.strategy = initial; }
void setStrategy(PaymentStrategy s) { this.strategy = s; } // смена в рантайме
void payOrder(double amount) {
strategy.pay(amount); // делегирование стратегии
}
}
Пример использования:
PaymentStrategy s1 = new CreditCardPayment("1234-****-****-5678");
CheckoutContext ctx = new CheckoutContext(s1);
ctx.payOrder(99.99); // платим картой
ctx.setStrategy(new PayPalPayment("user@example.com"));
ctx.payOrder(49.50); // теперь платим через PayPal
Пояснения к примеру:
- PaymentStrategy — интерфейс (контракт) для алгоритмов оплаты.
- Конкретные стратегии инкапсулируют разные реализации оплаты.
- CheckoutContext не знает деталей реализации, он просто делегирует вызов стратегии.
- Мы можем менять стратегию в рантайме методом setStrategy, что было бы неудобно или невозможно при чистом наследовании (требовало бы создавать новые подклассы для каждой комбинации поведения).
Это иллюстрирует основные преимущества шаблона «Стратегия» перед наследованием в описанных ситуациях.