Привет, Хабр!
Сегодня поговорим о паттерне «Компоновщик» (он же Composite) — на примере котиков. Котики идеально иллюстрируют структуру паттерна: в каждом доме есть простые котики, сложные котики (например, те, кто лазает по шкафам и открывает холодильники), а иногда — целые прайды из котиков.
Зачем нам Компоновщик?
Сам паттерн впервые был описан в книге «Design Patterns: Elements of Reusable Object‑Oriented Software». Его основная цель — упрощение работы с древовидными структурами.
Представим, что нужно написать приложение для управления зоопарком, где вольеры могут содержать как отдельных животных, так и группы животных. Нужно одинаково работать с «линейными» элементами (например, котиком Барсиком) и составными элементами (например, группой котиков, назовем её «Дворовая братва»).
Вот тут‑то хорошо зайдет Composite. Можно будет создавать древовидные структуры объектов, где клиентский код может обращаться к объектам одинаково, будь то лист (линейный объект) или узел (группа объектов).
Как выглядит структура:
-
Component — общий интерфейс или абстрактный класс для всех элементов структуры.
-
Leaf — конечный объект (в нашем случае, простой котик).
-
Composite — составной объект, который содержит другие объекты (например, группу котиков).
-
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 используется в:
-
GUI: компоненты интерфейса (кнопки, панели) организованы в древовидные структуры.
-
Файловые системы: папки содержат файлы и другие папки.
-
Организационные структуры: компании моделируют сотрудников и подразделения.
-
Текстовые редакторы: структура документа представлена с помощью Компоновщика.
Если у вас есть свои кейсы применения паттерна, делитесь в комментариях.
Больше про архитектуру приложений эксперты OTUS рассказывают в рамках практических онлайн-курсов — подробности в каталоге.
А в календаре мероприятий можно бесплатно записаться на открытые уроки по всем ИТ-направлениям.
ссылка на оригинал статьи https://habr.com/ru/articles/866508/
Добавить комментарий