Встраивание против агрегации
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy. Урок 38 из 107.
Вы узнали, что embedding продвигает поля и методы во внешнюю структуру. Но Go также поддерживает aggregation, при котором вы включаете другую структуру как именованное поле. Понимание того, когда использовать каждый подход, является ключом к хорошему проектированию.
При embedding имя field пропускается, и члены внутреннего типа становятся directly доступными:
type Writer struct{}
func (w Writer) Write() string { return "writing" }
type Document struct {
Writer // встраивание — без имени поля
}
d := Document{}
d.Write() // прямой доступ
При aggregation вы даёте полю явное имя, требуя использовать это имя для доступа к внутреннему типу:
type Document struct {
writer Writer // агрегация - именованное поле
}
d := Document{writer: Writer{}}
d.writer.Write() // необходимо использовать имя поля
Ключевое различие заключается в отношении, которое вы выражаете. Embedding подразумевает отношение «is-a», при котором внешний тип принимает идентичность внутреннего типа. Aggregation подразумевает отношение «has-a», при котором внешний тип лишь содержит внутренний тип.
Используйте embedding, когда хотите, чтобы внешний тип удовлетворял интерфейсам, реализованным внутренним типом, или когда direct доступ к методам делает API более понятным. Используйте aggregation, когда внутренний тип является деталью implementation, которую не следует раскрывать, или когда вам нужен explicit контроль над тем, как осуществляется доступ к внутреннему типу.
Задание
ЛегкоДавайте создадим систему генерации отчётов, которая демонстрирует, когда следует использовать embedding, а когда — aggregation. Вы создадите систему, в которой некоторые компоненты должны быть доступны напрямую (embedding), а другие должны оставаться внутренними деталями implementation (aggregation).
Вы организуете код в трёх файлах:
formatter.go: Создайте структуруFormatterс полемStyle(строка) и методомFormat(text string) string, который возвращает текст, обёрнутый в стиль, например[Style] text [/Style]. Это будет внутренний helper, который не следует предоставлять напрямую.report.go: Создайте две структуры, которые по-разному обрабатывают генерацию отчёта:- Структуру
Metadataс полямиAuthorиDate. Она представляет общедоступную information, к которой должен быть доступ напрямую - Структуру
Reportс полемTitle, которая embedsMetadata(для прямого доступа к автору и дате), но использует aggregation дляFormatter(как внутреннюю деталь implementation с именованным полемformatter)
ReportметодGenerate() string, который использует внутренний formatter для форматирования заголовка и включает metadata.- Структуру
main.go: Считайте details отчёта из входных данных, создайтеReportсо встроенными metadata и агрегированным formatter. Продемонстрируйте разницу, обратившись к автору напрямую через report (посредством embedding), а затем сгенерировав отформатированный отчёт (который внутренне использует скрытый formatter).
Будут предоставлены следующие входные данные:
- Строка 1: заголовок отчёта
- Строка 2: имя автора
- Строка 3: Date
- Строка 4: стиль Formatter
Ваш метод Format должен возвращать:
[Style] text [/Style]Ваш метод Generate должен возвращать:
[formatted title]
Author: [Author], Date: [Date]В вашем основном файле выведите три строки:
- Author, к которому выполнен прямой доступ через Report (что демонстрирует, как embedding делает field доступным)
- Date, к которому выполнен доступ через имя встроенного типа (
report.Metadata.Date) - Результат вызова
Generate()
Например, если заданы Quarterly Results, Jane Doe, 2024-01-15 и BOLD, результат должен быть следующим:
Jane Doe
2024-01-15
[BOLD] Quarterly Results [/BOLD]
Author: Jane Doe, Date: 2024-01-15Обратите внимание, что к Author и Date можно получить прямой доступ через Report посредством embedding, тогда как formatter остаётся скрытым. Нельзя напрямую вызвать report.Format(), потому что он агрегирован с именованным полем. На практике это демонстрирует отношение "is-a" versus "has-a".
Попробуйте сами
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
title := scanner.Text()
scanner.Scan()
author := scanner.Text()
scanner.Scan()
date := scanner.Text()
scanner.Scan()
style := scanner.Text()
// TODO: Создайте структуру Metadata с author и date
// TODO: Создайте Formatter с заданным style
// TODO: Создайте Report, который встраивает Metadata и агрегирует Formatter
// TODO: Выведите author, доступный напрямую через Report (встраивание продвигает поле)
// TODO: Выведите date, доступный через имя встроенного типа (report.Metadata.Date)
// TODO: Выведите результат вызова Generate()
// Подавите предупреждения о неиспользуемых переменных (удалите это, когда будете использовать переменные)
_ = title
_ = author
_ = date
_ = style
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
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Потренируйтесь самостоятельно: Онлайн-компилятор Go