golang

Паттерн Наблюдатель в Golang на котиках

  • вторник, 12 ноября 2024 г. в 00:00:08
https://habr.com/ru/companies/otus/articles/857330/

Привет, Хабр! Сегодня будем разбирать паттерн Наблюдатель на примере наших любимых пушистиков — котиков. Ведь кто, как не коты, могут быть идеальными субъектами и наблюдателями в нашем коде?

Коротко про сам паттерн

Паттерн Наблюдатель позволяет субъекту уведомлять зависимые объекты (наблюдателей) о произошедших изменениях. Допустим, у вас есть кот, который каждый раз, когда видит лазерную указку, начинает бегать за ней. Лазерная указка — это субъект, а коты — наблюдатели. Как только что-то изменилось (появился лазер), все коты получают сигнал и начинают действовать.

Реализация на Golang

Реализуем этот паттерн на Go, используя котиков в качестве примера. Представим, что есть лазерная указка, и несколько котов, которые наблюдают за её движениями.

Определяем интерфейсы

Первым делом определим интерфейсы Subject и Observer.

// Subject представляет лазерную указку
type Subject interface {
    RegisterObserver(o Observer) // Регистрация наблюдателя
    RemoveObserver(o Observer)   // Удаление наблюдателя
    NotifyObservers()            // Уведомление всех наблюдателей
}

// Observer представляет кота
type Observer interface {
    Update(position string) // Метод обновления состояния
}

Интерфейсы — это как команды для котов: они знают, что делать, но не как именно.

Реализуем лазерную указку

Теперь создадим структуру LaserPointer, которая будет субъектом.

type LaserPointer struct {
    observers []Observer
    position  string
}

// RegisterObserver добавляет кота в список наблюдателей
func (lp *LaserPointer) RegisterObserver(o Observer) {
    lp.observers = append(lp.observers, o)
}

// RemoveObserver убирает кота из списка наблюдателей
func (lp *LaserPointer) RemoveObserver(o Observer) {
    for i, observer := range lp.observers {
        if observer == o {
            lp.observers = append(lp.observers[:i], lp.observers[i+1:]...)
            break
        }
    }
}

// NotifyObservers уведомляет всех котов о новом положении лазера
func (lp *LaserPointer) NotifyObservers() {
    for _, observer := range lp.observers {
        observer.Update(lp.position)
    }
}

// Move меняет позицию лазера и уведомляет котов
func (lp *LaserPointer) Move(newPosition string) {
    lp.position = newPosition
    lp.NotifyObservers()
}

Создаем котов-наблюдателей

Теперь реализуем структуру Cat, которая будет наблюдателем.

func main() {
    laser := &LaserPointer{}

    cat1 := NewCat("Мурзик")
    cat2 := NewCat("Барсик")
    cat3 := NewCat("Симба")

    laser.RegisterObserver(cat1)
    laser.RegisterObserver(cat2)
    laser.RegisterObserver(cat3)

    positions := []string{"лево", "право", "вверх", "низ"}

    for _, pos := range positions {
        fmt.Printf("\nПеремещаем лазер в позицию: %s\n", pos)
        laser.Move(pos)
        time.Sleep(1 * time.Second) // Ждем секунду между перемещениями
    }

    // Убираем одного кота из гонки
    laser.RemoveObserver(cat2)
    fmt.Printf("\nБарсик устал и больше не будет гоняться за лазером.\n")

    laser.Move("центр")
}

Каждый кот — уникален. Но в нашем примере все коты — как один.

Собираем всё вместе

Теперь объединим компоненты в главной функции.

Перемещаем лазер в позицию: лево
Кот Мурзик заметил лазер в позиции: лево и начинает гоняться за ним!
Кот Барсик заметил лазер в позиции: лево и начинает гоняться за ним!
Кот Симба заметил лазер в позиции: лево и начинает гоняться за ним!

Перемещаем лазер в позицию: право
Кот Мурзик заметил лазер в позиции: право и начинает гоняться за ним!
Кот Барсик заметил лазер в позиции: право и начинает гоняться за ним!
Кот Симба заметил лазер в позиции: право и начинает гоняться за ним!

...

Барсик устал и больше не будет гоняться за лазером.
Кот Мурзик заметил лазер в позиции: центр и начинает гоняться за ним!
Кот Симба заметил лазер в позиции: центр и начинает гоняться за ним!

Видите, как коты реагируют на каждое перемещение лазера? Это именно то, что делает паттерн Наблюдатель.

Безопасность

Реализуем хоть какую-то безопасность.

  1. Потокобезопасность: если коты будут обрабатывать события параллельно, стоит использовать мьютексы или каналы для защиты общих ресурсов.

  2. Обработка ошибок: например, проверка на nil наблюдателей при уведомлении.

  3. Логирование: вместо fmt.Printf использовать логгер для более гибкого управления выводом.

Пример с потокобезопасностью:

import (
    "fmt"
    "sync"
    "time"
)

// LaserPointer с мьютексом для защиты списка наблюдателей
type LaserPointer struct {
    observers []Observer
    position  string
    mu        sync.Mutex
}

func (lp *LaserPointer) RegisterObserver(o Observer) {
    lp.mu.Lock()
    defer lp.mu.Unlock()
    lp.observers = append(lp.observers, o)
}

func (lp *LaserPointer) RemoveObserver(o Observer) {
    lp.mu.Lock()
    defer lp.mu.Unlock()
    for i, observer := range lp.observers {
        if observer == o {
            lp.observers = append(lp.observers[:i], lp.observers[i+1:]...)
            break
        }
    }
}

func (lp *LaserPointer) NotifyObservers() {
    lp.mu.Lock()
    defer lp.mu.Unlock()
    for _, observer := range lp.observers {
        // Запускаем обновление в отдельной горутине
        go observer.Update(lp.position)
    }
}

func (lp *LaserPointer) Move(newPosition string) {
    lp.position = newPosition
    lp.NotifyObservers()
}

Мьютекс защищает наш список котов от одновременных изменений. А горутины позволяют котикам гоняться за лазером параллельно.

Заключение

Теперь ваша очередь! Делитесь своими вариантами реализации и улучшениями в комментариях. Как вы используете этот паттерн в своем коде? Возможно, у вас есть свои фишки? Будет интересно обсудить!


Для Golang-разработчиков в ноябре пройдут два открытых урока:

  • 12 ноября: «Использование каналов в Go на практике». Вспомним теории об устройстве каналов, рассмотрим примеры их применения и ошибки при их использовании. Записаться

  • 21 ноября: «Пишем чистый Go‑код: паттерны». Особое внимание уделим singleflight, throttle, circuit breaker и другим популярным паттернам. Записаться