golang

Коты и Strategy в Go

  • вторник, 18 марта 2025 г. в 00:00:05
https://habr.com/ru/companies/otus/articles/890168/

Привет, Хабр!

Сегодня рассмотрим паттерн Strategy в Go на примере котиков — от простых стратегий поведения до динамической смены алгоритмов в многопоточном окружении.

Паттерн Strategy — это способ организации кода, при котором поведение объекта можно менять динамически, не изменяя его структуру. То есть мы не зашиваем логику прямо в класс (или структуру, если говорим о Go), а выносим её в отдельные стратегии — независимые объекты, реализующие единый интерфейс. Это избавляет от перегруженных if/else.

Сам паттерн хорошо применим, когда есть несколько вариантов поведения, которые могут взаимозаменяться. Например, есть кот, и его поведение зависит от настроения: играет, спит или царапает диван. Вместо того чтобы писать, if mood == "игривый" { играть() } else if mood == "сонный" { спать() } просто передаём коту нужную стратегию, и он ведёт себя соответствующе. Всё инкапсулировано и протестировано отдельно.

Постановка задачи

Допустим, есть приложение, где объекты (коты) могут менять своё поведение в зависимости от внешних условий. Вместо того чтобы зашивать всё в один монолитный метод, хочется отделить логику поведения от самой сущности. Это позволит:

  • Легко добавлять новые виды поведения.

  • Избежать переписывания кучи кода при изменении алгоритмов.

  • Сделать систему максимально гибкой.

Определяем интерфейс CatBehavior

Начнём с главного — определим, что должен уметь каждый алгоритм поведения. Для этого создаём интерфейс.

package main

// CatBehavior задаёт контракт для поведения кота.
// Каждый тип поведения должен реализовывать метод Act, возвращающий описание действия.
type CatBehavior interface {
	Act() string
}

Интерфейс позволяет в дальнейшем создавать сколько угодно реализаций, не меняя код, который его использует.

Реализуем конкретные стратегии

Стратегия «Игривый кот»

Представим, что кот сегодня решил играть. Напишем реализацию:

// PlayfulBehavior описывает поведение игривого кота.
type PlayfulBehavior struct{}

// Act возвращает описание того, как кот играет с лазерной указкой.
func (p *PlayfulBehavior) Act() string {
	return "играет с лазерной указкой"
}

Суть в том, что этот код — отдельный модуль.

Стратегия «Сонный кот»

А теперь, когда кот устал от игр и решил немного поспать:

// SleepyBehavior описывает поведение сонного кота.
type SleepyBehavior struct{}

// Act возвращает описание того, как кот нежно дремлет на подоконнике.
func (s *SleepyBehavior) Act() string {
	return "спит на подоконнике"
}

Метод возвращает строку, описывающую его действие.

Стратегия «Агрессивный кот»

И, наконец, для тех моментов, когда кот вдруг превращается в маленького диктатора:

// AggressiveBehavior описывает поведение агрессивного кота.
type AggressiveBehavior struct{}

// Act возвращает описание агрессивного действия кота.
func (a *AggressiveBehavior) Act() string {
	return "царапает всё подряд"
}

Создаем контекст

Теперь нужен объект, который использует эти стратегии. Допустим, есть кот, и его «настроение» можно менять динамически. Для этого создаём структуру Cat:

import (
	"fmt"
	"log"
	"sync"
)

// Cat – главный герой: кот с именем и текущим поведением.
type Cat struct {
	name     string
	behavior CatBehavior
	mutex    sync.RWMutex // Защищает смену поведения в многопоточной среде.
}

// NewCat создаёт нового кота. Если передать nil в качестве стратегии – мы сразу падаем.
func NewCat(name string, behavior CatBehavior) *Cat {
	if behavior == nil {
		log.Fatal("Ошибка: стратегия не может быть nil!")
	}
	return &Cat{
		name:     name,
		behavior: behavior,
	}
}

Меняем поведение

Добавим метод, который позволит менять стратегию на ходу, ведь кот может внезапно решиться сменить настроение:

// SetBehavior позволяет установить новую стратегию поведения для кота.
// Если передали nil, выводится предупреждение, а старая стратегия сохраняется.
func (c *Cat) SetBehavior(behavior CatBehavior) {
	if behavior == nil {
		log.Println("Предупреждение: попытка установить nil-стратегию – операция отменена!")
		return
	}
	c.mutex.Lock()
	defer c.mutex.Unlock()
	c.behavior = behavior
}

Мьютекс гарантирует, что если несколько горутин захотят изменить поведение одновременно, все будет под контролем.

Выполняем действие

А теперь — заставим кота что‑то делать, согласно текущей стратегии. Метод Act берет стратегию, аккуратно защищает её чтение мьютексом и выводит действие:

// Act заставляет кота выполнить текущее действие, описанное его стратегией.
func (c *Cat) Act() {
	c.mutex.RLock()
	defer c.mutex.RUnlock()
	fmt.Printf("Кот %s %s.\n", c.name, c.behavior.Act())
}

Сборка приложения

Начинаем с создания кота с первоначальной стратегией:

func main() {
	// Создаём кота с начальными настройками – пусть сегодня он играет.
	barsik := NewCat("Барсик", &PlayfulBehavior{})
	barsik.Act() // Ожидаем: "Кот Барсик играет с лазерной указкой."

Потом, когда приходит время смены настроения — наш кот решает поспать:

	// Барсик решил, что пора отдохнуть.
	barsik.SetBehavior(&SleepyBehavior{})
	barsik.Act() // Ожидаем: "Кот Барсик спит на подоконнике."

А если вдруг ему захочется показать, кто тут босс:

	// Барсик внезапно становится агрессивным.
	barsik.SetBehavior(&AggressiveBehavior{})
	barsik.Act() // Ожидаем: "Кот Барсик царапает всё подряд."

Теперь покажем, что наш код выдержит и многопоточную смену настроения.

	// Демонстрация смены поведения в многопоточном режиме.
	var wg sync.WaitGroup
	strategies := []CatBehavior{
		&PlayfulBehavior{},
		&SleepyBehavior{},
		&AggressiveBehavior{},
	}

	// Каждая горутина сменяет стратегию с небольшой задержкой.
	for i, strat := range strategies {
		wg.Add(1)
		go func(i int, strat CatBehavior) {
			defer wg.Done()
			// Имитация задержки (например, сетевые запросы, вычисления и т.п.)
			time.Sleep(time.Duration(i) * 100 * time.Millisecond)
			barsik.SetBehavior(strat)
			barsik.Act()
		}(i, strat)
	}
	wg.Wait()
}

Даже при параллельных изменениях, мьютекс гарантирует, что наш кот не окажется в неопределенном состояние.

Весь код
go
package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"sync"
	"time"
)

// CatBehavior задаёт контракт для поведения кота.
type CatBehavior interface {
	Act() string
}

// PlayfulBehavior – стратегия игривого кота.
type PlayfulBehavior struct{}

func (p *PlayfulBehavior) Act() string {
	return "играет с лазерной указкой"
}

// SleepyBehavior – стратегия сонного кота.
type SleepyBehavior struct{}

func (s *SleepyBehavior) Act() string {
	return "спит на подоконнике"
}

// AggressiveBehavior – стратегия агрессивного кота.
type AggressiveBehavior struct{}

func (a *AggressiveBehavior) Act() string {
	return "царапает всё подряд"
}

// PlayfulBehaviorWithEnergy – стратегия, зависящая от уровня энергии.
type PlayfulBehaviorWithEnergy struct {
	energy int // значение от 0 до 100
}

func (p *PlayfulBehaviorWithEnergy) Act() string {
	if p.energy > 70 {
		return "бурно гоняется за лазерной указкой"
	} else if p.energy > 30 {
		return "играет с мячиком"
	}
	return "лениво потирается о ногу хозяина"
}

// LoggedBehavior – декоратор для логирования вызовов стратегии.
type LoggedBehavior struct {
	inner CatBehavior
}

func (l *LoggedBehavior) Act() string {
	result := l.inner.Act()
	log.Printf("Лог: вызвана стратегия, результат: %s", result)
	return result
}

// Cat – структура, описывающая кота с именем и стратегией поведения.
type Cat struct {
	name     string
	behavior CatBehavior
	mutex    sync.RWMutex // Защищает смену поведения в многопоточном окружении.
}

// NewCat создаёт нового кота с заданной стратегией.
func NewCat(name string, behavior CatBehavior) *Cat {
	if behavior == nil {
		log.Fatal("Ошибка: стратегия не может быть nil!")
	}
	return &Cat{
		name:     name,
		behavior: behavior,
	}
}

// SetBehavior позволяет динамически менять стратегию поведения.
func (c *Cat) SetBehavior(behavior CatBehavior) {
	if behavior == nil {
		log.Println("Предупреждение: попытка установить nil-стратегию – операция отменена!")
		return
	}
	c.mutex.Lock()
	defer c.mutex.Unlock()
	c.behavior = behavior
}

// Act заставляет кота выполнить текущее действие согласно его стратегии.
func (c *Cat) Act() {
	c.mutex.RLock()
	defer c.mutex.RUnlock()
	fmt.Printf("Кот %s %s.\n", c.name, c.behavior.Act())
}

// BehaviorConfig описывает JSON-конфигурацию для выбора стратегии.
type BehaviorConfig struct {
	Type   string `json:"type"`
	Energy int    `json:"energy,omitempty"`
}

// StrategyFactory создаёт нужную стратегию по переданной конфигурации.
func StrategyFactory(config BehaviorConfig) (CatBehavior, error) {
	switch config.Type {
	case "playful":
		return &PlayfulBehavior{}, nil
	case "sleepy":
		return &SleepyBehavior{}, nil
	case "aggressive":
		return &AggressiveBehavior{}, nil
	case "playful_energy":
		return &PlayfulBehaviorWithEnergy{energy: config.Energy}, nil
	default:
		return nil, errors.New("неизвестный тип поведения")
	}
}

// demoParameterizedBehavior демонстрирует стратегию с параметрами.
func demoParameterizedBehavior() {
	murzik := NewCat("Мурзик", &PlayfulBehaviorWithEnergy{energy: 85})
	murzik.Act() // Ожидаем: "бурно гоняется за лазерной указкой"
	murzik.SetBehavior(&PlayfulBehaviorWithEnergy{energy: 20})
	murzik.Act() // Ожидаем: "лениво потирается о ногу хозяина"
}

// demoDynamicLoading демонстрирует динамическую загрузку стратегии из JSON.
func demoDynamicLoading() {
	configJSON := `{"type": "playful_energy", "energy": 90}`
	var cfg BehaviorConfig
	if err := json.Unmarshal([]byte(configJSON), &cfg); err != nil {
		log.Fatalf("Ошибка парсинга конфигурации: %v", err)
	}
	strategy, err := StrategyFactory(cfg)
	if err != nil {
		log.Fatalf("Ошибка создания стратегии: %v", err)
	}
	cat := NewCat("Динамик", strategy)
	cat.Act() // Ожидаем: "бурно гоняется за лазерной указкой"
}

// demoDecorator демонстрирует использование декоратора для логирования.
func demoDecorator() {
	baseBehavior := &SleepyBehavior{}
	loggedBehavior := &LoggedBehavior{inner: baseBehavior}
	cat := NewCat("Ленивый", loggedBehavior)
	cat.Act() // Логируется вызов стратегии.
}

func main() {
	// Создаем кота с первоначальной стратегией (игривость).
	barsik := NewCat("Барсик", &PlayfulBehavior{})
	barsik.Act() // Ожидаем: "Кот Барсик играет с лазерной указкой."

	// Меняем поведение на сонное.
	barsik.SetBehavior(&SleepyBehavior{})
	barsik.Act() // Ожидаем: "Кот Барсик спит на подоконнике."

	// Меняем поведение на агрессивное.
	barsik.SetBehavior(&AggressiveBehavior{})
	barsik.Act() // Ожидаем: "Кот Барсик царапает всё подряд."

	// Демонстрация смены поведения в многопоточном режиме.
	var wg sync.WaitGroup
	strategies := []CatBehavior{
		&PlayfulBehavior{},
		&SleepyBehavior{},
		&AggressiveBehavior{},
	}

	for i, strat := range strategies {
		wg.Add(1)
		go func(i int, strat CatBehavior) {
			defer wg.Done()
			time.Sleep(time.Duration(i) * 100 * time.Millisecond)
			barsik.SetBehavior(strat)
			barsik.Act()
		}(i, strat)
	}
	wg.Wait()

}

Параметризация и динамическая загрузка

Что если хочется добавить немного фич? Например, уровень энергии, влияющий на поведение кота. Допустим, чем выше энергия — тем активнее кот.

// PlayfulBehaviorWithEnergy – стратегия, где уровень энергии определяет стиль игры кота.
type PlayfulBehaviorWithEnergy struct {
	energy int // значение от 0 до 100
}

// Act возвращает действие кота в зависимости от его энергии.
func (p *PlayfulBehaviorWithEnergy) Act() string {
	if p.energy > 70 {
		return "бурно гоняется за лазерной указкой"
	} else if p.energy > 30 {
		return "играет с мячиком"
	}
	return "лениво потирается о ногу хозяина"
}

Пример использования:

func demoParameterizedBehavior() {
	// Кот с высокой энергией – настоящий атлет!
	murzik := NewCat("Мурзик", &PlayfulBehaviorWithEnergy{energy: 85})
	murzik.Act() // Ожидаем: "Кот Мурзик бурно гоняется за лазерной указкой."

	// Снижаем энергию – и кот становится более спокойным.
	murzik.SetBehavior(&PlayfulBehaviorWithEnergy{energy: 20})
	murzik.Act() // Ожидаем: "Кот Мурзик лениво потирается о ногу хозяина"
}

Допустим, приложение должно подстраиваться под внешние конфигурации — в этом случае можно загружать стратегию из JSON. Вот как это делается:

import (
	"encoding/json"
	"errors"
)

// BehaviorConfig описывает JSON-конфигурацию для выбора стратегии.
type BehaviorConfig struct {
	Type   string `json:"type"`
	Energy int    `json:"energy,omitempty"`
}

// StrategyFactory создаёт нужную стратегию по переданной конфигурации.
func StrategyFactory(config BehaviorConfig) (CatBehavior, error) {
	switch config.Type {
	case "playful":
		return &PlayfulBehavior{}, nil
	case "sleepy":
		return &SleepyBehavior{}, nil
	case "aggressive":
		return &AggressiveBehavior{}, nil
	case "playful_energy":
		return &PlayfulBehaviorWithEnergy{energy: config.Energy}, nil
	default:
		return nil, errors.New("неизвестный тип поведения")
	}
}

func demoDynamicLoading() {
	configJSON := `{"type": "playful_energy", "energy": 90}`
	var cfg BehaviorConfig
	if err := json.Unmarshal([]byte(configJSON), &cfg); err != nil {
		log.Fatalf("Ошибка парсинга конфигурации: %v", err)
	}
	strategy, err := StrategyFactory(cfg)
	if err != nil {
		log.Fatalf("Ошибка создания стратегии: %v", err)
	}
	cat := NewCat("Динамик", strategy)
	cat.Act() // Ожидаем: "Кот Динамик бурно гоняется за лазерной указкой"
}

Динамическая загрузка позволяет менять поведение без перекомпиляции.

Иногда хочется не только менять поведение, но и отслеживать его. Для этого можно применить паттерн Decorator. Обернём любую стратегию, чтобы логировать вызовы:

// LoggedBehavior – декоратор для логирования работы стратегии.
type LoggedBehavior struct {
	inner CatBehavior
}

// Act вызывает внутреннюю стратегию, логирует результат и возвращает его.
func (l *LoggedBehavior) Act() string {
	result := l.inner.Act()
	log.Printf("Лог: вызвана стратегия, результат: %s", result)
	return result
}

func demoDecorator() {
	baseBehavior := &SleepyBehavior{}
	loggedBehavior := &LoggedBehavior{inner: baseBehavior}
	cat := NewCat("Ленивый", loggedBehavior)
	cat.Act() // В логах появится запись о вызове стратегии.
}

Так можно обернуть логику дополнительной функциональностью.

Юнит-тест

Ничто не убеждает так, как хорошо написанные тесты.

package main

import "testing"

func TestPlayfulBehavior(t *testing.T) {
	var behavior CatBehavior = &PlayfulBehavior{}
	result := behavior.Act()
	expected := "играет с лазерной указкой"
	if result != expected {
		t.Errorf("Ожидалось %s, получили %s", expected, result)
	}
}

func TestSleepyBehavior(t *testing.T) {
	var behavior CatBehavior = &SleepyBehavior{}
	result := behavior.Act()
	expected := "спит на подоконнике"
	if result != expected {
		t.Errorf("Ожидалось %s, получили %s", expected, result)
	}
}

func TestAggressiveBehavior(t *testing.T) {
	var behavior CatBehavior = &AggressiveBehavior{}
	result := behavior.Act()
	expected := "царапает всё подряд"
	if result != expected {
		t.Errorf("Ожидалось %s, получили %s", expected, result)
	}
}

func TestPlayfulBehaviorWithEnergy(t *testing.T) {
	behavior := &PlayfulBehaviorWithEnergy{energy: 85}
	result := behavior.Act()
	expected := "бурно гоняется за лазерной указкой"
	if result != expected {
		t.Errorf("Ожидалось %s, получили %s", expected, result)
	}

	behavior.energy = 25
	result = behavior.Act()
	expected = "лениво потирается о ногу хозяина"
	if result != expected {
		t.Errorf("Ожидалось %s, получили %s", expected, result)
	}
}

Больше актуальных навыков по архитектуре приложений можно освоить на онлайн-курсах OTUS: в каталоге можно посмотреть список всех программ, а в календаре — записаться на открытые уроки.