Composite (Компоновщик)
Проблема
У вас есть древовидная структура: файлы и директории, UI-виджеты и контейнеры, задачи и подзадачи. Клиент хочет работать с отдельными элементами и группами одинаково -- через единый интерфейс.
Диаграмма
+-------------+
| Component | <-- interface
+-------------+
| Size() |
| Name() |
| Print(indent)|
+-------------+
^
+----+----+
| |
+--+---+ +--+--------+
| File | | Directory |
+------+ +------------+
| name | | name |
| size | | children[] |
+------+ +------------+
| Add() |
| Remove() |
+------------+
/project
+-- main.go (150B)
+-- pkg/
| +-- handler.go (300B)
| +-- service.go (450B)
+-- go.mod (50B)
Total: 950B
Реализация: Файловая система
package fs
import (
"fmt"
"strings"
)
// Component is the common interface for files and directories.
type Component interface {
Name() string
Size() int64
Print(indent int)
}
// --- File (leaf) ---
// File represents a single file.
type File struct {
name string
size int64
}
// NewFile creates a new file.
func NewFile(name string, size int64) *File {
return &File{name: name, size: size}
}
func (f *File) Name() string { return f.name }
func (f *File) Size() int64 { return f.size }
func (f *File) Print(indent int) {
prefix := strings.Repeat(" ", indent)
fmt.Printf("%s%s (%d bytes)\n", prefix, f.name, f.size)
}
// --- Directory (composite) ---
// Directory contains files and other directories.
type Directory struct {
name string
children []Component
}
// NewDirectory creates a new directory.
func NewDirectory(name string) *Directory {
return &Directory{name: name}
}
func (d *Directory) Name() string { return d.name }
// Size returns the total size of all children recursively.
func (d *Directory) Size() int64 {
var total int64
for _, child := range d.children {
total += child.Size()
}
return total
}
func (d *Directory) Print(indent int) {
prefix := strings.Repeat(" ", indent)
fmt.Printf("%s%s/ (%d bytes total)\n", prefix, d.name, d.Size())
for _, child := range d.children {
child.Print(indent + 1)
}
}
// Add adds a component to the directory.
func (d *Directory) Add(components ...Component) *Directory {
d.children = append(d.children, components...)
return d
}
// Remove removes a component by name.
func (d *Directory) Remove(name string) bool {
for i, child := range d.children {
if child.Name() == name {
d.children = append(d.children[:i], d.children[i+1:]...)
return true
}
}
return false
}
Использование
func main() {
// Build the tree
root := fs.NewDirectory("project")
pkg := fs.NewDirectory("pkg")
pkg.Add(
fs.NewFile("handler.go", 300),
fs.NewFile("service.go", 450),
)
root.Add(
fs.NewFile("main.go", 150),
pkg,
fs.NewFile("go.mod", 50),
)
// Client works with Component interface uniformly
root.Print(0)
fmt.Printf("\nTotal project size: %d bytes\n", root.Size())
// Output:
// project/ (950 bytes total)
// main.go (150 bytes)
// pkg/ (750 bytes total)
// handler.go (300 bytes)
// service.go (450 bytes)
// go.mod (50 bytes)
//
// Total project size: 950 bytes
}
Реальный пример: система задач
package task
import (
"fmt"
"strings"
)
// Task is the component interface.
type Task interface {
Title() string
IsComplete() bool
Progress() float64 // 0.0 to 1.0
Print(indent int)
}
// --- Simple task (leaf) ---
type SimpleTask struct {
title string
complete bool
}
func NewSimpleTask(title string) *SimpleTask {
return &SimpleTask{title: title}
}
func (t *SimpleTask) Title() string { return t.title }
func (t *SimpleTask) IsComplete() bool { return t.complete }
func (t *SimpleTask) Complete() { t.complete = true }
func (t *SimpleTask) Progress() float64 {
if t.complete {
return 1.0
}
return 0.0
}
func (t *SimpleTask) Print(indent int) {
prefix := strings.Repeat(" ", indent)
mark := " "
if t.complete {
mark = "x"
}
fmt.Printf("%s[%s] %s\n", prefix, mark, t.title)
}
// --- Task group (composite) ---
type TaskGroup struct {
title string
subtasks []Task
}
func NewTaskGroup(title string, tasks ...Task) *TaskGroup {
return &TaskGroup{title: title, subtasks: tasks}
}
func (g *TaskGroup) Title() string { return g.title }
func (g *TaskGroup) IsComplete() bool {
for _, t := range g.subtasks {
if !t.IsComplete() {
return false
}
}
return len(g.subtasks) > 0
}
func (g *TaskGroup) Progress() float64 {
if len(g.subtasks) == 0 {
return 0.0
}
var sum float64
for _, t := range g.subtasks {
sum += t.Progress()
}
return sum / float64(len(g.subtasks))
}
func (g *TaskGroup) Add(tasks ...Task) {
g.subtasks = append(g.subtasks, tasks...)
}
func (g *TaskGroup) Print(indent int) {
prefix := strings.Repeat(" ", indent)
fmt.Printf("%s%s (%.0f%%)\n", prefix, g.title, g.Progress()*100)
for _, t := range g.subtasks {
t.Print(indent + 1)
}
}
Использование:
func main() {
backend := task.NewTaskGroup("Backend",
task.NewSimpleTask("Setup database"),
task.NewSimpleTask("Create API endpoints"),
task.NewSimpleTask("Add authentication"),
)
frontend := task.NewTaskGroup("Frontend",
task.NewSimpleTask("Design mockups"),
task.NewSimpleTask("Implement UI"),
)
project := task.NewTaskGroup("Project v1.0", backend, frontend)
// Mark some tasks complete
backend.(*task.TaskGroup) // access subtasks...
project.Print(0)
fmt.Printf("\nOverall progress: %.0f%%\n", project.Progress()*100)
}
Обход дерева с функцией
// Walk traverses the tree and calls fn for each component.
func Walk(c Component, fn func(Component)) {
fn(c)
if dir, ok := c.(*Directory); ok {
for _, child := range dir.children {
Walk(child, fn)
}
}
}
// Find searches for a component by name.
func Find(c Component, name string) (Component, bool) {
if c.Name() == name {
return c, true
}
if dir, ok := c.(*Directory); ok {
for _, child := range dir.children {
if found, ok := Find(child, name); ok {
return found, true
}
}
}
return nil, false
}
Когда использовать
Используйте, когда:
- Данные имеют древовидную структуру
- Клиент должен работать с элементами и контейнерами одинаково
- Нужна рекурсивная композиция (задачи содержат подзадачи)
Не используйте, когда:
- Структура плоская (достаточно простого слайса)
- Элементы и контейнеры имеют принципиально разные операции
- Глубина вложенности фиксирована и мала
Сравнение с похожими паттернами
| Паттерн | Отличие |
|---|---|
| Decorator | Добавляет поведение к одному объекту; Composite организует дерево |
| Iterator | Обходит структуру; Composite определяет структуру |
| Visitor | Добавляет операции к дереву Composite |