MidТеория4 min

Composite

Паттерн Composite в Go: древовидные структуры с единым интерфейсом для элементов и контейнеров

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

Проверь себя

Какой ключевой принцип паттерна Composite?

Как метод Size() директории вычисляет суммарный размер?

Связанные темы