5 паттернов проектирования в Go на примере котиков

от автора

Привет, Хабр! Сегодня мы рассмотрим реализацию паттернов проектирования на Go, и, чтобы было не скучно, возьмем главными героями котиков. Будем разбирать 5 популярных паттернов: Singleton, Factory Method, Strategy, Observer, Decorator.

Singleton

Паттерн будет хорош тогда, когда в компании есть один кот, который отвечает, например, за доступ к единственной миске с едой, мы хотим быть уверены, что он существует в единственном экземпляре, и все остальные обращаются к нему как к глобальному ресурсу. Допустим, есть global-cat, который знает, где самая вкусная еда, и поэтому мы делаем Singleton:

package main  import ( "fmt" "sync" )  // SupremeCat — структура для Singleton type SupremeCat struct { Name string }  var ( once       sync.Once supremeCat *SupremeCat )  // GetSupremeCat — возвращает единственный экземпляр SupremeCat func GetSupremeCat() *SupremeCat { once.Do(func() { fmt.Println("Создаётся экземпляр SupremeCat...") supremeCat = &SupremeCat{Name: "Барсик Великий"} }) return supremeCat }  func main() { cat1 := GetSupremeCat() cat2 := GetSupremeCat()  if cat1 == cat2 { fmt.Println("Мы имеем один и тот же единственный кот:", cat1.Name) } else { fmt.Println("Что-то пошло не так, у нас больше одного кота!") } }

sync.Once гарантирует, что инициализация кота пройдет ровно один раз даже в многопоточной среде — никакой гонки.

Singleton нужен там, где необходимо гарантировать существование ровно одного экземпляра объекта. Это может быть:

  • Connection pool к базе данных. Например, если приложение работает с одной базой, и мы хотим избежать создания лишних соединений.

  • Логгер. Зачем плодить несколько логгеров, если можно централизовать запись логов?

  • Кеши или конфигурации. Общий доступ к конфигурации приложения — классический случай.

Factory Method

Представим, что есть разные типы котов: домашний кот (ленивый, может лежать на диване и мурлыкать) и уличный кот (хулиганистый, любит охотиться за голубями). Мы хотим иметь удобный фабричный метод, который, по какому-то параметру, будет создавать нужный тип кота, не раскрывая деталей его конструктора.

package main  import ( "errors" "fmt" )  // Cat — интерфейс для котов type Cat interface { Meow() GetLifestyle() string }  // HomeCat — домашний кот type HomeCat struct { name string }  func (h *HomeCat) Meow() { fmt.Println(h.name, "спокойно мурлычет на диване.") }  func (h *HomeCat) GetLifestyle() string { return "Домашний кот, любит кушать и спать." }  // StreetCat — уличный кот type StreetCat struct { name string }  func (s *StreetCat) Meow() { fmt.Println(s.name, "громко орёт во дворе и ворует сосиски.") }  func (s *StreetCat) GetLifestyle() string { return "Уличный кот, охотник и бандит." }  // NewCat — фабричный метод для создания котов func NewCat(catType, name string) (Cat, error) { if name == "" { return nil, errors.New("имя кота не может быть пустым") }  switch catType { case "home": return &HomeCat{name: name}, nil case "street": return &StreetCat{name: name}, nil default: return nil, fmt.Errorf("неизвестный тип кота: %s", catType) } }  func main() { // Создаём домашнего кота cat1, err := NewCat("home", "Пушок") if err != nil { fmt.Println("Ошибка создания кота:", err) return } cat1.Meow() fmt.Println(cat1.GetLifestyle())  // Создаём уличного кота cat2, err := NewCat("street", "Рыжик") if err != nil { fmt.Println("Ошибка создания кота:", err) return } cat2.Meow() fmt.Println(cat2.GetLifestyle())  // Пробуем создать кота с неизвестным типом cat3, err := NewCat("wild", "Дикий") if err != nil { fmt.Println("Ошибка создания кота:", err) return } cat3.Meow() fmt.Println(cat3.GetLifestyle()) }

Мы скрываем конкретные реализации за фабричным методом NewCat. Можно быстро подкинуть новый тип кота, не меняя код клиентов — просто дополнить фабрику.

Фабричный метод идеален, когда:

  • Есть семейство объектов с разными реализациями, но клиентский код не должен зависеть от деталей их создания.

  • Нужно централизовать и стандартизировать логику создания объектов.

Strategy

Представим, что есть кот, который в зависимости от ситуации мяукает по-разному. Иногда он «мяучит просяще», когда хочет поесть. Иногда — грозно, когда защищает территорию. Иногда тихо, когда пытается пробраться в комнату незаметно. Хочется подменять поведение «мяу» на лету, не меняя класс кота.

package main  import "fmt"  // MeowStrategy — интерфейс для стратегий мяуканья type MeowStrategy interface { Meow(name string) }  // QuietMeow — тихое мяуканье type QuietMeow struct{}  func (q *QuietMeow) Meow(name string) { fmt.Println(name, "тихо и незаметно подмяукивает...") }  // BeggingMeow — жалобное мяуканье type BeggingMeow struct{}  func (b *BeggingMeow) Meow(name string) { fmt.Println(name, "жалобно просит еду: \"Мяу! Мяу!\"") }  // AngryMeow — агрессивное мяуканье type AngryMeow struct{}  func (a *AngryMeow) Meow(name string) { fmt.Println(name, "яростно шипит и орёт: \"МЯУ!\"") }  // FlexibleCat — кот с изменяемым поведением мяуканья type FlexibleCat struct { name        string meowBehavior MeowStrategy }  // PerformMeow вызывает текущее поведение мяуканья func (f *FlexibleCat) PerformMeow() { if f.meowBehavior == nil { // Проверка на nil fmt.Println(f.name, "не знает, как мяукать!") return } f.meowBehavior.Meow(f.name) }  // SetMeowStrategy устанавливает новую стратегию мяуканья func (f *FlexibleCat) SetMeowStrategy(strategy MeowStrategy) { if strategy == nil { fmt.Println("Невозможно установить пустую стратегию для", f.name) return } f.meowBehavior = strategy fmt.Println(f.name, "сменил стиль мяуканья!") }  func main() { // Создаём кота с тихим мяуканьем cat := &FlexibleCat{name: "Тишка", meowBehavior: &QuietMeow{}} cat.PerformMeow()  // Меняем поведение на жалобное cat.SetMeowStrategy(&BeggingMeow{}) cat.PerformMeow()  // Меняем поведение на агрессивное cat.SetMeowStrategy(&AngryMeow{}) cat.PerformMeow()  // Пробуем установить nil стратегию cat.SetMeowStrategy(nil) cat.PerformMeow() }

Strategy полезен, когда:

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

  • Нужно инкапсулировать поведение и сделать код более тестируемым.

Observer

Представим, что есть кот, который совершает некие действия (например, ест, спит, играет), и есть другие компоненты системы, которым надо знать, что кот сделал что-то интересное. Например, есть мониторинг, который отслеживает, сколько раз кот кушал, чтобы не дать ему переесть. Есть логгер, который записывает все действия кота. Observer паттерн позволит подписаться на события кота и автоматически оповещать подписчиков.

package main  import ( "fmt" "strings" )  // Observer — интерфейс для наблюдателей type Observer interface { Notify(action string) }  // CatSubject — кот, на чьи действия подписываются наблюдатели type CatSubject struct { observers []Observer name      string }  // Attach добавляет наблюдателя func (c *CatSubject) Attach(o Observer) { if o == nil { return } c.observers = append(c.observers, o) }  // Detach удаляет наблюдателя func (c *CatSubject) Detach(o Observer) { newObservers := make([]Observer, 0, len(c.observers)) for _, obs := range c.observers { if obs != o { newObservers = append(newObservers, obs) } } c.observers = newObservers }  // NotifyAll оповещает всех наблюдателей func (c *CatSubject) NotifyAll(action string) { for _, obs := range c.observers { if obs != nil { obs.Notify(fmt.Sprintf("%s: %s", c.name, action)) } } }  // LogObserver пишет в лог действия кота type LogObserver struct{}  func (l *LogObserver) Notify(action string) { fmt.Println("[LOG]:", action) }  // HealthObserver следит за количеством еды type HealthObserver struct { eatCount int }  func (h *HealthObserver) Notify(action string) { if strings.Contains(action, "ест") { h.eatCount++ if h.eatCount > 3 { fmt.Println("[HEALTH]: Кот переел! Нужно ограничить доступ к миске!") } else { fmt.Printf("[HEALTH]: Кот покушал. Всего раз: %d\n", h.eatCount) } } else { fmt.Println("[HEALTH]: Кот делает что-то не связанное с едой.") } }  // Действия кота func (c *CatSubject) Eat() { c.NotifyAll("ест корм") }  func (c *CatSubject) Sleep() { c.NotifyAll("сладко спит") }  func (c *CatSubject) Play() { c.NotifyAll("играет с мячиком") }  func main() { cat := &CatSubject{name: "Мурзик"} logObs := &LogObserver{} healthObs := &HealthObserver{}  // Подписываем наблюдателей cat.Attach(logObs) cat.Attach(healthObs)  // Действия кота cat.Eat() cat.Eat() cat.Play() cat.Eat() cat.Eat() cat.Sleep()  // Отписываем логгера cat.Detach(logObs)  // Действия после отписки cat.Eat() }

Логи пишутся, здоровье мониторится.

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

  • Уведомления в UI.

  • Логирование событий.

Decorator

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

package main  import "fmt"  // SimpleCat — интерфейс кота type SimpleCat interface { Meow() }  // BaseCat — базовый кот type BaseCat struct { name string }  func (b *BaseCat) Meow() { fmt.Println(b.name, "просто говорит: 'Мяу!'") }  // LoggingCatDecorator — декоратор, логирующий мяуканье type LoggingCatDecorator struct { cat SimpleCat }  func (l *LoggingCatDecorator) Meow() { if l.cat == nil { fmt.Println("[LOGGING ERROR]: Нет кота для логирования!") return } fmt.Println("[LOGGING BEFORE]: Кот готовится к мяу.") l.cat.Meow() fmt.Println("[LOGGING AFTER]: Кот уже мяукнул.") }  // NotifyingCatDecorator — декоратор, отправляющий уведомления type NotifyingCatDecorator struct { cat SimpleCat }  func (n *NotifyingCatDecorator) Meow() { if n.cat == nil { fmt.Println("[NOTIFY ERROR]: Нет кота для уведомления!") return } fmt.Println("[NOTIFY]: Отправляем сообщение в Slack канал #cats") n.cat.Meow() }  func main() { // Создаём базового кота cat := &BaseCat{name: "Котофей"}  // Добавляем декоратор логирования loggedCat := &LoggingCatDecorator{cat: cat}  // Добавляем декоратор уведомлений notifiedAndLoggedCat := &NotifyingCatDecorator{cat: loggedCat}  // Обычный кот fmt.Println("=== Обычный кот ===") cat.Meow()  // Кот с логированием fmt.Println("\n=== Кот с логированием ===") loggedCat.Meow()  // Кот с логированием и уведомлением fmt.Println("\n=== Кот с логированием и уведомлением ===") notifiedAndLoggedCat.Meow() }

В продакшне декоратор — классический способ расширить функционал без ломки существующего кода. Можно цеплять дополнительные фичи к чему угодно: расширять логирование, добавлять трейсинг, кэширование, всё что угодно. Тут кот просто мяукает, а мы сверху навешиваем логгер и отправку сообщения.


Заключение

Как видите, не важно, что у нас за доменная область: коты, собаки, сервисы аутентификации или раздатчики пиццы — паттерны универсальны. Главное, понять, где применить их грамотно, и помнить, что паттерн — это не панацея, а инструмент.

Хочу порекомендовать вам бесплатные вебинары курса «Архитектура и шаблоны проектирования»:


ссылка на оригинал статьи https://habr.com/ru/articles/864748/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *