Привет, Хабр!
Сегодня рассмотрим паттерн 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: в каталоге можно посмотреть список всех программ, а в календаре — записаться на открытые уроки.
