Паттерн Компоновщик
Часть раздела Объектно-ориентированное программирование путешествия по 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Компоновщик идеален, когда вам нужно представить иерархии «часть-целое» и вы хотите, чтобы клиенты одинаково трактовали отдельные объекты и их композиции.
Задание
ЛегкоДавайте создадим систему организационной структуры, используя паттерн Компоновщик (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)
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП в Go
Внешние файлыРабочее пространство и модули GoПакеты и импортыЭкспортируемые и неэкспортируемые именаВведение в ООП в GoСтруктуры как классыОпределение методов структурПолучатели-указатели и получатели-значенияИнициализация структурФункции-конструкторыИтоги — Простой калькулятор4Интерфейсы
Введение в интерфейсыНеявная реализацияИнтерфейс как контрактПустой интерфейс (any)Утверждение типаПереключатель типовКомпозиция интерфейсовИнтерфейсы Stringer и ErrorПовторение: Калькулятор фигур7Инкапсуляция
Экспортируемые и неэкспортируемые поляИнкапсуляция на уровне пакетовГеттеры и сеттерыСокрытие информации в GoИтоги — Записи о студентах10Обобщения (Generics) (Go 1.18+)
Введение в GenericsПараметры типовОграничения типовОбобщенные структурыОбходной путь для обобщенных методовИтоги — Обобщенная коллекция13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonПаттерн FactoryПаттерн Abstract FactoryПаттерн ObserverПаттерн Strategy2Глубокое погружение в типы и структуры
Базовые и составные типыОпределение пользовательских типовТеги структурАнонимные структурыВложенные структурыНулевые значения и значения по умолчаниюПовторение — Контактная книга5Композиция вместо наследования
Почему в Go нет наследованияОсновы встраивания структурПродвижение методовВстраивание нескольких структурВстраивание против агрегацииЗатенение встроенных методовИтоги — Иерархия сотрудников8Обработка ошибок и ООП
Интерфейс errorПользовательские типы ошибокОбертывание ошибок (fmt.Errorf)Sentinel-ошибкиerrors.Is() и errors.As()Panic, Defer и RecoverИтоги — Парсер файлов11Стандартная библиотека и ООП
io.Reader и io.Writersort.InterfaceИнтерфейс fmt.Stringerencoding/json со структурамиИнтерфейс http.HandlerПовторение: модели REST API14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикMiddleware как Декоратор3Указатели и память
Основы указателей в GoУказатели на структурыПередача по значению и по ссылкеФункция new()Сборка мусора в GoПовторение: Конструктор связного списка6Полиморфизм в Go
Полиморфизм через интерфейсыУтиная типизация в GoПравила реализации интерфейсовПолиморфные коллекцииВнедрение зависимостейИтоги — Обработчик платежей9Конкурентность и ООП
Основы горутинКаналы и взаимодействиеБуферизованные и небуферизованные каналыОператор selectsync.Mutex и sync.RWMutexsync.WaitGroupПроектирование потокобезопасных структурПовторение — Worker Pool