Паттерн Composite в Go на котиках

cdc6611e14f9a4073f31787b90d9e9fc.png

Привет, Хабр!

Сегодня поговорим о паттерне «Компоновщик» (он же Composite) — на примере котиков. Котики идеально иллюстрируют структуру паттерна: в каждом доме есть простые котики, сложные котики (например, те, кто лазает по шкафам и открывает холодильники), а иногда — целые прайды из котиков.

Зачем нам Компоновщик?

Сам паттерн впервые был описан в книге «Design Patterns: Elements of Reusable Object‑Oriented Software». Его основная цель — упрощение работы с древовидными структурами.

Представим, что нужно написать приложение для управления зоопарком, где вольеры могут содержать как отдельных животных, так и группы животных. Нужно одинаково работать с «линейными» элементами (например, котиком Барсиком) и составными элементами (например, группой котиков, назовем её «Дворовая братва»).

Вот тут‑то хорошо зайдет Composite. Можно будет создавать древовидные структуры объектов, где клиентский код может обращаться к объектам одинаково, будь то лист (линейный объект) или узел (группа объектов).

Как выглядит структура:

  1. Component — общий интерфейс или абстрактный класс для всех элементов структуры.

  2. Leaf — конечный объект (в нашем случае, простой котик).

  3. Composite — составной объект, который содержит другие объекты (например, группу котиков).

  4. Client — клиентский код, который работает со всем этим великолепием.

Но перед тем как рассмотреть реализацию паттерна, стоит задуматься:, а почему не использовать что‑то попроще? Например, обычные массивы или кастомные структуры данных.

На первый взгляд, идея хранить котиков в массиве кажется простой и удобной. Но стоит лишь попытаться добавить в такой массив «группу котиков», как возникнет проблема: как отличить одиночного котика от целой группы? А если группы могут содержать другие группы? Придется вводить дополнительные флаги или проверять типы объектов, что очевидно приведет к громоздкому и трудночитаемому коду.

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

  • Усложнение логики: вы быстро упретесь в необходимость повторять уже готовые решения, включая интерфейсы, наследование и композицию.

  • Расширяемость: добавление нового поведения для всех элементов структуры может стать проблемо.

Именно поэтому Composite — это классическое решение, которое делает код гибким, расширяемым, а главное понятным.

Реализация

Начнём с базового интерфейса. Котики будут обладать поведением «Мяукать». Вот так выглядит интерфейс:

package main

import "fmt"

// Component — общий интерфейс для котиков и их групп

type Cat interface {
    Meow()
}

Теперь создадим класс для одиночных котиков:

// Leaf — одиночный котик

type SimpleCat struct {
    name string
}

func (sc *SimpleCat) Meow() {
    fmt.Printf("%s: Мяу!\n", sc.name)
}

// Конструктор для котиков
func NewSimpleCat(name string) *SimpleCat {
    return &SimpleCat{name: name}
}

Вот мы создали простого котика. Давайте проверим:

func main() {
    barsik := NewSimpleCat("Барсик")
    barsik.Meow() // Барсик: Мяу!
}

Отлично. Но что, если у нас целая группа котиков?

Напишем составной класс, который будет представлять группу котиков:

// Composite — группа котиков

type CatGroup struct {
    name  string
    cats  []Cat
}

func (cg *CatGroup) Meow() {
    fmt.Printf("%s: Начинаем общий концерт:\n", cg.name)
    for _, cat := range cg.cats {
        cat.Meow()
    }
}

func (cg *CatGroup) Add(cat Cat) {
    cg.cats = append(cg.cats, cat)
}

func NewCatGroup(name string) *CatGroup {
    return &CatGroup{name: name, cats: []Cat{}}
}

Этот класс позволяет добавлять котиков и вызывать их «мяуканье» рекурсивно. Проверим его:

func main() {
    barsik := NewSimpleCat("Барсик")
    murzik := NewSimpleCat("Мурзик")

    dvorniki := NewCatGroup("Дворовая братва")
    dvorniki.Add(barsik)
    dvorniki.Add(murzik)

    dvorniki.Meow()
    // Дворовая братва: Начинаем общий концерт:
    // Барсик: Мяу!
    // Мурзик: Мяу!
}

Теперь создадим группу котиков, которая содержит другие группы котиков. В Компоновщике это делается просто:

func main() {
    barsik := NewSimpleCat("Барсик")
    murzik := NewSimpleCat("Мурзик")

    dvorniki := NewCatGroup("Дворовая братва")
    dvorniki.Add(barsik)
    dvorniki.Add(murzik)

    aristokraty := NewCatGroup("Аристократы")
    aristokraty.Add(NewSimpleCat("Людовик"))
    aristokraty.Add(NewSimpleCat("Шарль"))

    zoo := NewCatGroup("Зоопарк")
    zoo.Add(dvorniki)
    zoo.Add(aristokraty)

    zoo.Meow()
    // Зоопарк: Начинаем общий концерт:
    // Дворовая братва: Начинаем общий концерт:
    // Барсик: Мяу!
    // Мурзик: Мяу!
    // Аристократы: Начинаем общий концерт:
    // Людовик: Мяу!
    // Шарль: Мяу!
}

Теперь есть настоящая древовидная структура котиков! Можно писать приложение для управления ими.

Что еще можно добавить?

Можно расширить функционал. Добавим методы Remove и GetChild для управления группами:

func (cg *CatGroup) Remove(cat Cat) {
    for i, c := range cg.cats {
        if c == cat {
            cg.cats = append(cg.cats[:i], cg.cats[i+1:]...)
            return
        }
    }
}

func (cg *CatGroup) GetChild(index int) (Cat, error) {
    if index < 0 || index >= len(cg.cats) {
        return nil, fmt.Errorf("индекс %d вне диапазона", index)
    }
    return cg.cats[index], nil
}

А также расширим характеристики котиков, добавив возраст и породу:

// Обновлённый интерфейс Cat
type Cat interface {
    Meow()
    GetInfo() string
}

// Обновлённый SimpleCat
type SimpleCat struct {
    name  string
    age   int
    breed string
}

func (sc *SimpleCat) Meow() {
    fmt.Printf("%s: Мяу! (%d лет, порода: %s)\n", sc.name, sc.age, sc.breed)
}

func (sc *SimpleCat) GetInfo() string {
    return fmt.Sprintf("Котик: %s, Возраст: %d, Порода: %s", sc.name, sc.age, sc.breed)
}

func NewSimpleCat(name string, age int, breed string) *SimpleCat {
    return &SimpleCat{name: name, age: age, breed: breed}
}

// Обновлённый CatGroup
func (cg *CatGroup) GetInfo() string {
    return fmt.Sprintf("Группа котиков: %s, Количество: %d", cg.name, len(cg.cats))
}

Плюсом подключим горутины для параллельного мяуканья:

import (
    "fmt"
    "sync"
)

func (cg *CatGroup) Meow() {
    fmt.Printf("%s: Начинаем общий концерт:\n", cg.name)
    var wg sync.WaitGroup
    for _, cat := range cg.cats {
        wg.Add(1)
        go func(c Cat) {
            defer wg.Done()
            c.Meow()
        }(cat)
    }
    wg.Wait()
}

Так группы котиков могут мяукать одновременно, что открывает доступ к моделированию более сложных сценариев.

Где применять все это дело

Паттерн Composite используется в:

  1. GUI: компоненты интерфейса (кнопки, панели) организованы в древовидные структуры.

  2. Файловые системы: папки содержат файлы и другие папки.

  3. Организационные структуры: компании моделируют сотрудников и подразделения.

  4. Текстовые редакторы: структура документа представлена с помощью Компоновщика.

Если у вас есть свои кейсы применения паттерна, делитесь в комментариях.

Больше про архитектуру приложений эксперты OTUS рассказывают в рамках практических онлайн-курсов — подробности в каталоге.

А в календаре мероприятий можно бесплатно записаться на открытые уроки по всем ИТ-направлениям.

© Habrahabr.ru