Шаблон «Стратегия»

В программной разработке при создании сопровождаемых, масштабируемых и гибких систем важны шаблоны проектирования. Шаблоном «Стратегия» определяется семейство алгоритмов с инкапсулированием каждого из них, ею они делаются взаимозаменяемыми. Особенно полезен этот шаблон в сценариях, где стратегии или поведения применяются динамически без изменения кодовой базы.

Когда?

  1. Динамические изменения поведения: когда поведение объекта нужно поменять динамически во время выполнения, исходя из условий.
  2. Уход от логики условного перехода: чтобы избежать нагромождения операторов if-else или switch при выборе алгоритмов.
  3. Переиспользуемые алгоритмы: когда имеется набор связанных алгоритмов для взаимозаменяемого применения.

Для чего?

Шаблоном «Стратегия» обеспечивается четкое разделение обязанностей посредством делегирования логики поведения различным стратегиям классов. Так повышаются гибкость и масштабируемость при соблюдении принципов объектно-ориентированного проектирования.

Как?

Шаблоном «Стратегия» определяется интерфейс для семейства алгоритмов. Каждый алгоритм инкапсулируется конкретными реализациями интерфейса, при помощи которых классом контекста применяется желаемое поведение.

Этапы:

  1. Определяется интерфейс «Стратегии»: для операторов if-else / switch создается интерфейс, стандартная функциональность в if-else / switch обобщается методами интерфейса.
  2. Создаются конкретные стратегии для каждого из операторов if-else.
  3. Для динамического управления отбором и выполнением стратегий используется класс контекста.

Где применяется шаблон «Стратегия»

  1. Платежные шлюзы: при выборе способов оплаты (кредитная карта, PayPal, криптовалюта).
  2. Алгоритмы сортировки: переключение между быстрой сортировкой, сортировкой слиянием или пузырьком в зависимости от размера данных.
  3. Сжатие: применение различных стратегий сжатия (ZIP, RAR, GZIP).
  4. Аутентификация: выбор механизмов аутентификации (OAuth, JWT, LDAP).
  5. Машинное обучение: динамическое применение алгоритмов вроде линейной регрессии, ансамбля случайного леса.

Код

Обобщенный код

package main
import "fmt"
func main() {
num1, num2 := 10, 5
operation := "add"
// Базовая логика «if-else» для различных операций
switch operation {
case "add":
fmt.Printf("Addition Result: %d\n", num1+num2)
case "subtract":
fmt.Printf("Subtraction Result: %d\n", num1-num2)
case "multiply":
fmt.Printf("Multiplication Result: %d\n", num1*num2)
case "divide":
if num2 != 0 {
fmt.Printf("Division Result: %d\n", num1/num2)
} else {
fmt.Println("Error: Division by zero")
}
default:
fmt.Println("Unknown operation")
}
}

Хороший код

package main
import "fmt"

// Этап 1: определяется интерфейс «Стратегии»
type OperationStrategy interface {
Execute(a, b int) int
}
// Этап 2: для каждой операции создаются конкретные стратегии
type AddStrategy struct{}

func (s AddStrategy) Execute(a, b int) int {
return a + b
}
type SubtractStrategy struct{}

func (s SubtractStrategy) Execute(a, b int) int {
return a - b
}

type MultiplyStrategy struct{}
func (s MultiplyStrategy) Execute(a, b int) int {
return a * b
}

type DivideStrategy struct{}
func (s DivideStrategy) Execute(a, b int) int {
if b != 0 {
return a / b
}
fmt.Println("Error: Division by zero")
return 0
}

// Этап 3: применяется класс контекста
type Calculator struct {
Strategy OperationStrategy
}

func (c *Calculator) SetStrategy(strategy OperationStrategy) {
c.Strategy = strategy
}

func (c Calculator) Calculate(a, b int) int {
return c.Strategy.Execute(a, b)
}

func main() {
calculator := Calculator{}

calculator.SetStrategy(AddStrategy{})
fmt.Printf("Addition Result: %d\n", calculator.Calculate(10, 5))

calculator.SetStrategy(SubtractStrategy{})
fmt.Printf("Subtraction Result: %d\n", calculator.Calculate(10, 5))

calculator.SetStrategy(MultiplyStrategy{})
fmt.Printf("Multiplication Result: %d\n", calculator.Calculate(10, 5))

calculator.SetStrategy(DivideStrategy{})
fmt.Printf("Division Result: %d\n", calculator.Calculate(10, 5))
}

Соотносим это с примером шаблона «Фабрика»:

package main
import (
"fmt"
"time"
)

// «Task» — это универсальная задача с общими атрибутами
type Task struct {
ID string
CreatedAt time.Time
Description string
}

// Поведение для выполняемых задач определяется в «TaskStrategy»
// Этап 1: определяется интерфейс «Стратегии»
type TaskStrategy interface {
Execute()
}

// Операции баз данных управляются в «DatabaseTask»
// Этап 2: создаются конкретные стратегии
type DatabaseTask struct {
Task
Query string
DBName string
Connected bool
}
func (dt *DatabaseTask) Execute() {
if !dt.Connected {
fmt.Printf("[DatabaseTask] Connecting to database: %s\n", dt.DBName)
dt.Connected = true
}
fmt.Printf("[DatabaseTask] Executing query: %s\n", dt.Query)
}

type APITask struct {
Task
URL string
Headers map[string]string
}
func (at *APITask) Execute() {
fmt.Printf("[APITask] Sending request to URL: %s with headers: %v\n", at.URL, at.Headers)
}

type FileProcessingTask struct {
Task
FilePath string
FileType string
}
func (fpt *FileProcessingTask) Execute() {
fmt.Printf("[FileProcessingTask] Processing file: %s of type: %s\n", fpt.FilePath, fpt.FileType)
}

// Выполнение стратегий управляется в «TaskContext»
// Этап 3: применяется класс контекста
type TaskContext struct {
Strategy TaskStrategy
}
func (tc *TaskContext) SetStrategy(strategy TaskStrategy) {
tc.Strategy = strategy
}
func (tc TaskContext) ExecuteTask() {
tc.Strategy.Execute()
}

func main() {
context := TaskContext{}
// Конфигурирование «DatabaseTask»
dbTask := &DatabaseTask{
Task: Task{
ID: "DB001",
CreatedAt: time.Now(),
Description: "Run a database query",
},
Query: "SELECT * FROM users",
DBName: "UserDB",
}
context.SetStrategy(dbTask) // «DatabaseTask» становится стратегией
context.ExecuteTask()

// Конфигурирование «APITask»
apiTask := &APITask{
Task: Task{
ID: "API001",
CreatedAt: time.Now(),
Description: "Send an API request",
},
URL: "https://api.example.com/data",
Headers: map[string]string{"Authorization": "Bearer token", "Content-Type": "application/json"},
}
context.SetStrategy(apiTask) // «APITask» становится стратегией
context.ExecuteTask()

// Конфигурирование «FileProcessingTask»
fileTask := &FileProcessingTask{
Task: Task{
ID: "FILE001",
CreatedAt: time.Now(),
Description: "Process a CSV file",
},
FilePath: "/data/files/report.csv",
FileType: "CSV",
}
context.SetStrategy(fileTask)
context.ExecuteTask()
}

Заключение

«Стратегия»  —  это поведенческий шаблон проектирования для динамического выбора алгоритмов или поведений во время выполнения с инкапсулированием их в отдельные классы. Особенно полезен он для упрощения кода, осложненного операторами if-else или switch, при повышении гибкости и соблюдении принципа открытости/закрытости.

Благодаря определению общего интерфейса для стратегий, созданию конкретных реализаций для каждого алгоритма и управлению их выполнением с помощью класса контекста, шаблоном «Стратегия» обеспечиваются четкое разделение обязанностей, повышенная тестопригодность и более легкая масштабируемость. Он активно применяется в таких сценариях, как обработка платежей, сортировка, сжатие и системы аутентификации. Более того, с этим шаблоном также используется отношение has-a.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Shantanu Saini: Go Strategy Pattern

Предыдущая статья10 практик написания кода, на которые полагаются все старшие разработчики
Следующая статьяРеверсинг плагина компилятора Compose: перехват фронтенда