Привет, Хабр! Сегодня мы рассмотрим реализацию паттернов проектирования на 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()
}
В продакшне декоратор — классический способ расширить функционал без ломки существующего кода. Можно цеплять дополнительные фичи к чему угодно: расширять логирование, добавлять трейсинг, кэширование, всё что угодно. Тут кот просто мяукает, а мы сверху навешиваем логгер и отправку сообщения.
Заключение
Как видите, не важно, что у нас за доменная область: коты, собаки, сервисы аутентификации или раздатчики пиццы — паттерны универсальны. Главное, понять, где применить их грамотно, и помнить, что паттерн — это не панацея, а инструмент.
Хочу порекомендовать вам бесплатные вебинары курса "Архитектура и шаблоны проектирования":
11.12. Создание микросервиса. Зарегистрироваться
18.12. Обработка исключений и SOLID. Зарегистрироваться