Коты и Strategy в Go
- вторник, 18 марта 2025 г. в 00:00:05
Привет, Хабр!
Сегодня рассмотрим паттерн Strategy в Go на примере котиков — от простых стратегий поведения до динамической смены алгоритмов в многопоточном окружении.
Паттерн Strategy — это способ организации кода, при котором поведение объекта можно менять динамически, не изменяя его структуру. То есть мы не зашиваем логику прямо в класс (или структуру, если говорим о Go), а выносим её в отдельные стратегии — независимые объекты, реализующие единый интерфейс. Это избавляет от перегруженных if/else.
Сам паттерн хорошо применим, когда есть несколько вариантов поведения, которые могут взаимозаменяться. Например, есть кот, и его поведение зависит от настроения: играет, спит или царапает диван. Вместо того чтобы писать, if mood == "игривый" { играть() } else if mood == "сонный" { спать() }
просто передаём коту нужную стратегию, и он ведёт себя соответствующе. Всё инкапсулировано и протестировано отдельно.
Допустим, есть приложение, где объекты (коты) могут менять своё поведение в зависимости от внешних условий. Вместо того чтобы зашивать всё в один монолитный метод, хочется отделить логику поведения от самой сущности. Это позволит:
Легко добавлять новые виды поведения.
Избежать переписывания кучи кода при изменении алгоритмов.
Сделать систему максимально гибкой.
Начнём с главного — определим, что должен уметь каждый алгоритм поведения. Для этого создаём интерфейс.
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: в каталоге можно посмотреть список всех программ, а в календаре — записаться на открытые уроки.