MidТеория4 min

Strategy

Паттерн Strategy в Go: взаимозаменяемые алгоритмы через интерфейсы, функциональные типы и sort.Interface

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 -- алгоритм

Проверь себя

Когда использовать функциональный тип вместо интерфейса для Strategy?

Чем Strategy отличается от State?

Какой интерфейс стандартной библиотеки Go является примером Strategy?

Связанные темы