Menu
Coddy logo textTech

Паттерн Компоновщик

Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy — урок 96 из 107.

Паттерн Компоновщик (Composite) позволяет единообразно работать с отдельными объектами и группами объектов. В то время как Состояние (State) изменяет поведение в зависимости от внутреннего состояния, Компоновщик строит древовидные структуры, где как листья, так и контейнеры имеют общий интерфейс.

Этот паттерн идеально подходит для иерархических структур, таких как файловые системы, организационные диаграммы или компоненты пользовательского интерфейса. В Go мы определяем общий интерфейс, который реализуют как отдельные элементы, так и контейнеры:

type Component interface {
    GetSize() int
}

type File struct {
    Name string
    Size int
}

func (f File) GetSize() int {
    return f.Size
}

Компоновщик (контейнер) содержит дочерние элементы и реализует тот же интерфейс, агрегируя их результаты:

type Folder struct {
    Name     string
    Children []Component
}

func (f *Folder) Add(c Component) {
    f.Children = append(f.Children, c)
}

func (f Folder) GetSize() int {
    total := 0
    for _, child := range f.Children {
        total += child.GetSize()
    }
    return total
}

Теперь вы можете вкладывать папки в папки, и вызов GetSize() работает одинаково, независимо от того, является ли это отдельным файлом или целым деревом каталогов:

docs := &Folder{Name: "docs"}
docs.Add(File{Name: "readme.txt", Size: 100})
docs.Add(File{Name: "notes.txt", Size: 50})

root := &Folder{Name: "root"}
root.Add(docs)
root.Add(File{Name: "config.json", Size: 25})

fmt.Println(root.GetSize())  // 175

Компоновщик идеален, когда вам нужно представить иерархии «часть-целое» и вы хотите, чтобы клиенты одинаково трактовали отдельные объекты и их композиции.

challenge icon

Задание

Легко

Давайте создадим систему организационной структуры, используя паттерн Компоновщик (Composite)! Вы создадите структуру, в которой как отдельные сотрудники, так и отделы (содержащие сотрудников или другие отделы) могут рассматриваться единообразно — это идеально подходит для расчета общей заработной платы в любой части организации.

Вы организуете свой код в трех файлах:

  • component.go: Определите общий интерфейс, который будут реализовывать как отдельные лица, так и группы.

    Создайте интерфейс OrgComponent с двумя методами:

    • GetSalary() int — возвращает общую зарплату для этого компонента
    • GetName() string — возвращает имя сотрудника или название отдела
  • organization.go: Реализуйте типы листа (отдельный сотрудник) и компоновщика (отдел).

    Создайте структуру Employee с полями Name (string) и Salary (int). Ее метод GetSalary() возвращает собственную зарплату, а GetName() возвращает имя.

    Создайте структуру Department с полем Name (string) и срезом Members, который содержит элементы OrgComponent. Добавьте метод Add(c OrgComponent) для добавления участников. Ее метод GetSalary() должен возвращать сумму зарплат всех участников, а GetName() возвращает название отдела.

  • main.go: Постройте структуру организации и рассчитайте зарплаты.

    Считайте количество сотрудников. Для каждого сотрудника считайте его имя и зарплату, создавая экземпляры Employee. Затем считайте количество отделов. Для каждого отдела считайте его название и количество индексов участников, а затем сами эти индексы (начиная с 0, ссылаясь на ранее созданных сотрудников или отделы в порядке их создания).

    После построения структуры считайте финальный индекс и выведите имя этого компонента и общую зарплату в формате: [name]: [salary]

Будут предоставлены следующие входные данные:

  • Количество сотрудников, затем имя и зарплата каждого сотрудника на отдельных строках
  • Количество отделов, затем название каждого отдела, количество участников и индексы участников
  • Финальный индекс для запроса

Например, при вводе:

3
Alice
50000
Bob
60000
Carol
55000
1
Engineering
3
0
1
2
3

Ваш вывод должен быть:

Engineering: 165000

И при вводе:

4
Alice
50000
Bob
60000
Carol
55000
Dave
70000
2
Dev
2
0
1
QA
2
2
3
4

Ваш вывод должен быть:

QA: 125000

И при вводе:

2
Alice
50000
Bob
60000
0
1

Ваш вывод должен быть:

Bob: 60000

Обратите внимание, как GetSalary() работает одинаково, вызываете ли вы его для одного сотрудника или для целого отдела — паттерн Компоновщик позволяет вам единообразно работать с отдельными объектами и группами через один и тот же интерфейс!

Шпаргалка

Паттерн Компоновщик (Composite) позволяет единообразно работать с отдельными объектами и группами объектов через общий интерфейс. Он идеально подходит для иерархических структур, таких как файловые системы, организационные диаграммы или компоненты пользовательского интерфейса.

Определите общий интерфейс, который реализуют как отдельные элементы, так и контейнеры:

type Component interface {
    GetSize() int
}

Реализуйте лист (отдельный элемент):

type File struct {
    Name string
    Size int
}

func (f File) GetSize() int {
    return f.Size
}

Реализуйте компоновщик (контейнер), который содержит дочерние элементы и агрегирует их результаты:

type Folder struct {
    Name     string
    Children []Component
}

func (f *Folder) Add(c Component) {
    f.Children = append(f.Children, c)
}

func (f Folder) GetSize() int {
    total := 0
    for _, child := range f.Children {
        total += child.GetSize()
    }
    return total
}

Пример использования, демонстрирующий единообразную работу с отдельными элементами и композициями:

docs := &Folder{Name: "docs"}
docs.Add(File{Name: "readme.txt", Size: 100})
docs.Add(File{Name: "notes.txt", Size: 50})

root := &Folder{Name: "root"}
root.Add(docs)
root.Add(File{Name: "config.json", Size: 25})

fmt.Println(root.GetSize())  // 175

Попробуйте сами

package main

import "fmt"

func main() {
	// Считать количество сотрудников
	var numEmployees int
	fmt.Scanln(&numEmployees)

	// Хранить все компоненты (сотрудников и отделы) в порядке создания
	var components []OrgComponent

	// TODO: Считать имя и зарплату каждого сотрудника
	// Создать экземпляры Employee и добавить их в слайс components
	for i := 0; i < numEmployees; i++ {
		var name string
		var salary int
		fmt.Scanln(&name)
		fmt.Scanln(&salary)
		// TODO: Создать сотрудника и добавить в components
	}

	// Считать количество отделов
	var numDepartments int
	fmt.Scanln(&numDepartments)

	// TODO: Считать название каждого отдела, количество участников и их индексы
	// Создать экземпляры Department, добавить участников по индексу и добавить в components
	for i := 0; i < numDepartments; i++ {
		var deptName string
		var memberCount int
		fmt.Scanln(&deptName)
		fmt.Scanln(&memberCount)
		// TODO: Создать отдел, считать индексы участников, добавить участников, добавить в components
	}

	// Считать финальный индекс для запроса
	var queryIndex int
	fmt.Scanln(&queryIndex)

	// TODO: Вывести имя компонента и общую зарплату в формате: [name]: [salary]
	// Пример: fmt.Printf("%s: %d\n", name, salary)
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование