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

от автора

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

Сегодня поговорим о паттерне «Компоновщик» (он же 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 рассказывают в рамках практических онлайн-курсов — подробности в каталоге.

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


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