Pull to refresh
876.36
OTUS
Цифровые навыки от ведущих экспертов

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

Level of difficultyEasy
Reading time7 min
Views5.5K

Привет, Хабр! Сегодня мы рассмотрим реализацию паттернов проектирования на 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()
}

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


Заключение

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

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

Tags:
Hubs:
+6
Comments6
6

Articles

Information

Website
otus.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
OTUS