Strategy (Стратегия)
Проблема
Нужно выбирать алгоритм в runtime без изменения клиентского кода. Например: разные способы оплаты, алгоритмы сжатия, стратегии кэширования. Strategy позволяет менять алгоритм, передавая его как зависимость.
В Go Strategy реализуется двумя способами: через интерфейс (для сложных стратегий) и через функциональный тип (для простых).
Диаграмма
Context (PaymentProcessor)
+---------------------+
| strategy Strategy |
| Process(amount) |
+---------------------+
|
+---> CreditCard Strategy
+---> PayPal Strategy
+---> Crypto Strategy
Или через функцию:
type CompressFunc func([]byte) ([]byte, error)
Реализация: Интерфейсная Strategy
package payment
import (
"context"
"fmt"
)
// Strategy defines the payment algorithm.
type Strategy interface {
Pay(ctx context.Context, amount float64) (transactionID string, err error)
Name() string
}
// --- Credit Card ---
type creditCard struct {
number string
cvv string
expiry string
}
func NewCreditCard(number, cvv, expiry string) Strategy {
return &creditCard{number: number, cvv: cvv, expiry: expiry}
}
func (c *creditCard) Name() string { return "credit_card" }
func (c *creditCard) Pay(ctx context.Context, amount float64) (string, error) {
// Process credit card payment via gateway
masked := "****" + c.number[len(c.number)-4:]
return fmt.Sprintf("CC-%s-%.0f", masked, amount*100), nil
}
// --- PayPal ---
type paypal struct {
email string
token string
}
func NewPayPal(email, token string) Strategy {
return &paypal{email: email, token: token}
}
func (p *paypal) Name() string { return "paypal" }
func (p *paypal) Pay(ctx context.Context, amount float64) (string, error) {
// Process PayPal payment
return fmt.Sprintf("PP-%s-%.0f", p.email, amount*100), nil
}
// --- Context (Processor) ---
// Processor uses a payment strategy.
type Processor struct {
strategy Strategy
}
// NewProcessor creates a payment processor with the given strategy.
func NewProcessor(strategy Strategy) *Processor {
return &Processor{strategy: strategy}
}
// SetStrategy changes the payment strategy at runtime.
func (p *Processor) SetStrategy(s Strategy) {
p.strategy = s
}
// Checkout processes a payment using the current strategy.
func (p *Processor) Checkout(ctx context.Context, amount float64) (string, error) {
if p.strategy == nil {
return "", fmt.Errorf("no payment strategy set")
}
fmt.Printf("Processing $%.2f via %s\n", amount, p.strategy.Name())
return p.strategy.Pay(ctx, amount)
}
Использование:
func main() {
ctx := context.Background()
processor := payment.NewProcessor(payment.NewCreditCard("4111111111111111", "123", "12/25"))
txn, err := processor.Checkout(ctx, 99.99)
fmt.Printf("Transaction: %s, Error: %v\n", txn, err)
// Switch strategy at runtime
processor.SetStrategy(payment.NewPayPal("[email protected]", "token"))
txn, err = processor.Checkout(ctx, 49.99)
fmt.Printf("Transaction: %s, Error: %v\n", txn, err)
}
Реализация: Функциональная Strategy
Для простых стратегий интерфейс -- overkill. Функциональный тип проще.
package compress
import (
"bytes"
"compress/gzip"
"compress/zlib"
"fmt"
"io"
)
// CompressFunc is a functional strategy for compression.
type CompressFunc func(data []byte) ([]byte, error)
// Gzip compression strategy.
func Gzip(data []byte) ([]byte, error) {
var buf bytes.Buffer
w := gzip.NewWriter(&buf)
if _, err := w.Write(data); err != nil {
return nil, fmt.Errorf("gzip write: %w", err)
}
if err := w.Close(); err != nil {
return nil, fmt.Errorf("gzip close: %w", err)
}
return buf.Bytes(), nil
}
// Zlib compression strategy.
func Zlib(data []byte) ([]byte, error) {
var buf bytes.Buffer
w := zlib.NewWriter(&buf)
if _, err := w.Write(data); err != nil {
return nil, fmt.Errorf("zlib write: %w", err)
}
if err := w.Close(); err != nil {
return nil, fmt.Errorf("zlib close: %w", err)
}
return buf.Bytes(), nil
}
// NoCompression strategy (passthrough).
func NoCompression(data []byte) ([]byte, error) {
result := make([]byte, len(data))
copy(result, data)
return result, nil
}
// Archiver uses a compression strategy.
type Archiver struct {
compress CompressFunc
}
func NewArchiver(strategy CompressFunc) *Archiver {
return &Archiver{compress: strategy}
}
func (a *Archiver) Archive(data []byte) ([]byte, error) {
return a.compress(data)
}
Использование:
func main() {
data := []byte("Hello, World! " + strings.Repeat("Go is great! ", 100))
// Try different compression strategies
strategies := map[string]compress.CompressFunc{
"gzip": compress.Gzip,
"zlib": compress.Zlib,
"none": compress.NoCompression,
}
for name, strategy := range strategies {
archiver := compress.NewArchiver(strategy)
compressed, _ := archiver.Archive(data)
ratio := float64(len(compressed)) / float64(len(data)) * 100
fmt.Printf("%s: %d -> %d bytes (%.1f%%)\n", name, len(data), len(compressed), ratio)
}
}
sort.Interface как Strategy
Стандартная библиотека Go использует Strategy через sort.Interface:
package main
import (
"fmt"
"sort"
)
type Employee struct {
Name string
Salary float64
Age int
}
// --- Strategy: sort by salary ---
type BySalary []Employee
func (s BySalary) Len() int { return len(s) }
func (s BySalary) Less(i, j int) bool { return s[i].Salary < s[j].Salary }
func (s BySalary) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// --- Strategy: sort by age ---
type ByAge []Employee
func (s ByAge) Len() int { return len(s) }
func (s ByAge) Less(i, j int) bool { return s[i].Age < s[j].Age }
func (s ByAge) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func main() {
employees := []Employee{
{"Alice", 90000, 30},
{"Bob", 75000, 25},
{"Charlie", 85000, 35},
}
sort.Sort(BySalary(employees))
fmt.Println("By salary:", employees)
sort.Sort(ByAge(employees))
fmt.Println("By age:", employees)
// Or with slices.SortFunc (modern Go):
slices.SortFunc(employees, func(a, b Employee) int {
return cmp.Compare(a.Salary, b.Salary)
})
}
Интерфейс vs Функция: когда что
| Критерий | Интерфейс | Функция |
|---|---|---|
| Состояние | Стратегия имеет конфигурацию/состояние | Чистая функция без состояния |
| Сложность | Несколько методов | Один метод |
| Тестирование | Mock через интерфейс | Подстановка функции |
| Пример | PaymentStrategy (name + pay) | CompressFunc |
Когда использовать
Используйте, когда:
- Несколько взаимозаменяемых алгоритмов
- Алгоритм выбирается в runtime (по конфигурации, пользователю)
- Нужно изолировать алгоритм от контекста
- Тестирование требует подмены алгоритма
Не используйте, когда:
- Один алгоритм, который не меняется
- Алгоритмы слишком разные для общего интерфейса
- Выбор тривиален (достаточно if/switch)
Сравнение с похожими паттернами
| Паттерн | Отличие |
|---|---|
| State | Автоматические переходы; Strategy -- клиент выбирает |
| Template Method | Фиксированная структура с вариативными шагами; Strategy -- полная замена |
| Command | Инкапсулирует действие; Strategy -- алгоритм |