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