В этой статье я объясню концепцию составного шаблона проектирования, цели, плюсы и минусы, сценарии и способы реализации, а также предоставлю два экземпляра и модульные тесты.
Нажмите, чтобы стать средним участником и читать неограниченное количество историй!
Концепция
Составной шаблон — это структурный шаблон проектирования, который позволяет объединять объекты в древовидную структуру и работать с ней, как если бы это был отдельный объект. Он позволяет объединять объекты в древовидные структуры, а затем работать с этими структурами, как если бы они были отдельными объектами. Шаблон использует единый интерфейс для представления как отдельных объектов, так и групп объектов. Клиенты могут взаимодействовать с объектами, не зная, имеют ли они дело с одним объектом или с группой объектов.
Цели
Целями составного шаблона являются:
- Обеспечьте способ обработки отдельных объектов и их композиций единообразно.
- Упростите клиентский код, последовательно обрабатывая объекты.
- Упростите добавление новых компонентов без изменения существующего кода.
За и против
Преимущества использования составного шаблона:
- Упрощает клиентский код, единообразно обрабатывая отдельные объекты и их композиции.
- Позволяет легко добавлять новые типы компонентов.
- Поощряет использование принципа единой ответственности путем разделения различных проблем.
Недостатки использования составного шаблона:
- Это может сделать дизайн слишком общим, что затруднит его понимание.
- Это может привести к дополнительной сложности при попытке управлять отношениями между компонентами.
Сценарии
Составной шаблон полезен в следующих случаях:
1. Обработка данных JSON
Шаблон Composite можно использовать для моделирования данных JSON в виде древовидной структуры, где объекты и массивы являются составными узлами, а примитивные типы — конечными узлами. Реализуя общий интерфейс для этих узлов, вы можете единообразно манипулировать данными JSON, упрощая добавление, удаление или изменение узлов.
2. Приложение для графического рисования
Шаблон Composite можно использовать для представления фигур и групп фигур в виде древовидной структуры. Отдельные фигуры являются листовыми узлами, а группы фигур — составными узлами. Реализуя общий интерфейс для обоих типов узлов, вы можете единообразно обрабатывать отдельные фигуры и группы фигур, упрощая управление ими.
3. Система контроля доступа
Шаблон Composite можно использовать для моделирования контроля доступа в здание в виде древовидной структуры, где зоны являются составными узлами, а точки контроля доступа (например, двери, турникеты) — конечными узлами. Реализуя общий интерфейс для этих узлов, вы можете единообразно управлять контролем доступа для зон и вложенных зон, упрощая применение ограничений доступа и обновление системы контроля доступа.
Как реализовать
Ниже приведены шаги по реализации составного шаблона в Go.
- Определите интерфейс компонента.
- Реализуйте структуру листьев.
- Реализуйте составную структуру.
- Используйте шаблон Composite в клиентском коде.
Помните об указанных выше шагах при реализации составного шаблона.
Первый случай
В следующем примере я реализую пример дерева. Следующий код представляет собой содержимое файла Composite.go.
package tree import ( "log" ) // Step 1:Define the Component interface. type Component interface { Operation() } // Step 2:Implement the Leaf struct. type Leaf struct { } func (l *Leaf) Operation() { log.Println("Leaf operation") } // Step 4:Implement the Composite struct type Composite struct { children []Component } func (c *Composite) Add(component Component) { c.children = append(c.children, component) } func (c *Composite) Remove(component Component) { for i, child := range c.children { if child == component { c.children = append(c.children[:i], c.children[i+1:]...) break } if composite, ok := child.(*Composite); ok { composite.Remove(child) } } } func (c *Composite) Operation() { log.Println("Composite operation") for _, child := range c.children { child.Operation() } }
Приведенный выше код, интерфейс Component, имеет единственный метод Operation(). Leaf и Composite — это два типа компонентов. Структура Leaf представляет отдельный объект. Структура Composite представляет собой группу объектов. Составной тип имеет дочернее поле, в котором хранится фрагмент компонентов. Он имеет три метода: Add(), Remove() и Operation(). Методы Add() и Remove() позволяют добавлять и удалять компоненты из композита соответственно. Метод Operation выполняет операцию над композитом и его дочерними элементами. Я использую log.Println() для вывода, этот способ полезен в тестовом файле.
Приведенный ниже код является содержимым композитного_test.go. В тесте я использую закрытие теста для проверки всех методов и вывода.
package tree import ( "bytes" "fmt" "log" "testing" "github.com/go-playground/assert/v2" ) type OperationLogger struct { *bytes.Buffer } func TestComposite(t *testing.T) { leaf1 := &Leaf{} leaf2 := &Leaf{} composite := &Composite{} t.Run("Test default composite", func(t *testing.T) { assert.Equal(t, 0, len(composite.children)) if len(composite.children) != 0 { t.Errorf("Expected 0 children, got %d", len(composite.children)) } }) t.Run("Test add component", func(t *testing.T) { composite.Add(leaf1) // Use the Composite pattern in client code. composite.Add(leaf2) // Use the Composite pattern in client code. assert.Equal(t, 2, len(composite.children)) if len(composite.children) != 2 { t.Errorf("Expected 2 children, got %d", len(composite.children)) } }) t.Run("Test remove component", func(t *testing.T) { composite.Add(leaf1) composite.Add(leaf2) composite.Remove(leaf2) assert.Equal(t, 3, len(composite.children)) if len(composite.children) != 3 { t.Errorf("Expected 1 child, got %d", len(composite.children)) } }) t.Run("Test operation method", func(t *testing.T) { leaf3 := &Leaf{} composite.Add(leaf3) logger := &OperationLogger{Buffer: &bytes.Buffer{}} log.SetOutput(logger) composite.Operation() if len(logger.String()) != 180 { t.Errorf("Unexpected output: %s", logger.String()) fmt.Printf("Actual bytes: %v\n", logger.String()) } }) }
В приведенном выше коде каждый t.Run() является тестовым закрытием. Сначала создайте Листья и Композит. Это общие переменные в течение всего теста. Здесь я использую 4 тестовых закрытия для выполнения тестов. В моем случае, несмотря на то, что тестовые закрытия разделены, не все тестовые закрытия могут быть запущены соответственно. Однако они могут успешно выполняться вместе. В последнем тестовом закрытии трудно протестировать контент напрямую, даже если вы можете распечатать почти тот же результат. Поскольку использование журнала для записи вывода будет включать текущее время. Вот почему я делаю это в длину. OperationLogger используется для записи выходных данных в буфер. Скриншот результата теста следующий.
Второй экземпляр
Ниже я реализую немного более сложный экземпляр. Следующий код также является содержимым файла Composite.go с тем же именем. Интерфейс Trainer имеет единственный метод Train(), а интерфейс Swimmer имеет единственный метод Swim(). Athlete, CompositeSwimmerA и CompositeSwimmerB — это три типа компонентов Trainer. SwimmerImplementor, Shark и CompositeSwimmerB — это три типа компонентов Swimmer. CompositeSwimmerB имеет два встроенных элемента, каждый из которых соответственно указывает на интерфейс. Это означает, что одна структура реализует два метода интерфейса.
package composition type Trainer interface { Train() string } type Swimmer interface { Swim() string } type Athlete struct{} func (a *Athlete) Train() string { return "Training" } func Swim() string { return "Swimming!" } type SwimmerImplementor struct{} func (s *SwimmerImplementor) Swim() string { return "Swimming!" } type CompositeSwimmerA struct { AthleteA Athlete SwimmerA *func() string } type CompositeSwimmerB struct { Trainer Swimmer } type Animal struct{} func (r *Animal) Eat() string { return "Eating" } type Shark struct { Animal Swim func() string }
Следующий код представляет собой содержимое файла Composite_test.go.
package composition import ( "testing" "github.com/go-playground/assert/v2" ) func TestAthleteTrain(t *testing.T) { athlete := Athlete{} assert.Equal(t, "Training", athlete.Train()) } func TestSwimmerASwim(t *testing.T) { localSwim := Swim swimmer := CompositeSwimmerA{ SwimmerA: &localSwim, } assert.Equal(t, "Training", swimmer.AthleteA.Train()) assert.Equal(t, "Swimming!", (*swimmer.SwimmerA)()) // SwimmerA is a closure in swimmer } func TestAnimalSwim(t *testing.T) { fish := Shark{ Swim: Swim, } assert.Equal(t, "Eating", fish.Eat()) assert.Equal(t, "Swimming!", fish.Swim()) } func TestSwimmerBSwim(t *testing.T) { swimmer := CompositeSwimmerB{ &Athlete{}, &SwimmerImplementor{}, } assert.Equal(t, "Training", swimmer.Train()) assert.Equal(t, "Swimming!", swimmer.Swim()) }
В приведенном выше коде я разделяю их на четыре теста.
Первый — самый простой, он просто проверяет реализацию метода Athlete Train().
Второй используется для тестирования CompositeSwimmerA. Поскольку я хочу использовать функцию Swim() в структуре CompositeSwimmerA, мне нужно сделать двухэтапный вызов. Во-первых, назначьте функцию Swim() для видимости localSwim, потому что у функции нет адреса для передачи ее типу CompositeSwimmerA. Во-вторых, возьмите localSwim и скопируйте его в метод SwimmerA. Особенность заключается в том, что (*swimmer.SwimmerA)()” является замыканием.
Третий используется для проверки того, что животные едят и плавают. В этой части Swim можно использовать напрямую, потому что SwimmerA из CompositeSwimmerA определен с типом функции указателя. Swim of Shark определил только тип функции. Таким образом, ему не нужно сначала скрывать адрес, его можно использовать напрямую. Метод fish.Eat(), который используется в качестве встроенных объектов. В Golang вы также можете встраивать объекты в объекты, чтобы они выглядели как наследование. Это означает, что нам не нужно будет явно вызывать имя поля, чтобы получить доступ к его полям и методам, потому что они будут частью нас.
Последний используется для тестирования CompositeSwimmerB, который включает в себя Athlete и SwimmerImplementor. Ключевым моментом является то, что CompositeSwimmerB имеет два встроенных элемента, каждый из которых соответственно указывает на интерфейс. Но испытание для них легкое.
Скриншот результатов теста ниже.
Заключение
При использовании шаблона проектирования Composite в Go нужно быть очень осторожным, чтобы не спутать его с наследованием. Проблема наследования заключается в том, что каждый раз, когда мы хотим добавить новый атрибут, нам придется добавлять его к определенным классам. Здесь на помощь приходит композит. Композит всегда проще расширить новым атрибутом.
Если вы будете следовать всему вышеизложенному, вы поймете, что такое составной шаблон, как реализовать составной шаблон с помощью Golang и как реализовать его модульные тесты.
Вернитесь к шаблонам структурного проектирования и нажмите здесь.
Чтобы просмотреть шаблоны креативного дизайна в Golang, нажмите здесь.
Чтобы просмотреть шаблоны поведенческого проектирования в Golang, нажмите здесь.
Спасибо, что читаете. Если вам понравилась моя статья, хлопайте в ладоши и подписывайтесь на меня. Я с удовольствием отвечу на все ваши вопросы, если вы спросите меня в комментарии. Нажмите на следующую ссылку, чтобы стать средним участником.
Нажмите, чтобы стать средним участником и читать неограниченное количество историй!