Встраивание против агрегации
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy — урок 38 из 107.
Вы узнали, что встраивание продвигает поля и методы во внешнюю struct. Но Go также поддерживает агрегацию, при которой вы включаете другую struct в качестве именованного поля. Понимание того, когда использовать каждый из подходов, является ключом к хорошему проектированию.
При встраивании вы опускаете имя поля, и члены внутреннего типа становятся доступными напрямую:
type Writer struct{}
func (w Writer) Write() string { return "writing" }
type Document struct {
Writer // встраивание — без имени поля
}
d := Document{}
d.Write() // прямой доступ
При агрегации вы даете полю явное имя, что требует использования этого имени для доступа к внутреннему типу:
type Document struct {
writer Writer // агрегация — именованное поле
}
d := Document{writer: Writer{}}
d.writer.Write() // необходимо использовать имя поля
Ключевое различие заключается в типе отношений, которые вы выражаете. Встраивание (Embedding) предполагает отношение «is-a», при котором внешний тип принимает идентичность внутреннего типа. Агрегация (Aggregation) предполагает отношение «has-a», при котором внешний тип просто содержит внутренний тип.
Используйте встраивание, когда вы хотите, чтобы внешний тип соответствовал интерфейсам, реализованным внутренним типом, или когда прямой доступ к методам делает API чище. Используйте агрегацию, когда внутренний тип является деталью реализации, которая не должна быть открыта, или когда вам нужен явный контроль над тем, как осуществляется доступ к внутреннему типу.
Задание
ЛегкоДавайте создадим систему генерации отчетов, которая демонстрирует, когда использовать встраивание (embedding), а когда — агрегацию (aggregation). Вы создадите систему, в которой некоторые компоненты должны быть доступны напрямую (встраивание), в то время как другие должны оставаться внутренними деталями реализации (агрегация).
Вы организуете свой код в трех файлах:
formatter.go: Создайте структуруFormatterс полемStyle(string) и методомFormat(text string) string, который возвращает текст, обернутый тегами стиля, например[Style] text [/Style]. Это будет внутренний помощник, который не должен быть доступен напрямую.report.go: Создайте две структуры, которые по-разному обрабатывают генерацию отчетов:- Структура
Metadataс полямиAuthorиDate— она представляет общедоступную информацию, которая должна быть доступна напрямую. - Структура
Reportс полемTitle, которая встраивает (embeds)Metadata(для прямого доступа к автору и дате), но использует агрегацию дляFormatter(как внутреннюю деталь реализации с именованным полемformatter).
Generate() stringвReport, который использует внутренний форматер для форматирования заголовка и включает метаданные.- Структура
main.go: Считайте данные отчета из входного потока, создайтеReportс использованием как встроенных метаданных, так и агрегированного форматера. Продемонстрируйте разницу, обратившись к автору напрямую через отчет (благодаря встраиванию), а затем сгенерировав отформатированный отчет (который использует скрытый форматер внутри).
Будут предоставлены следующие входные данные:
- Строка 1: Заголовок отчета
- Строка 2: Имя автора
- Строка 3: Дата
- Строка 4: Стиль форматера
Ваш метод Format должен возвращать:
[Style] text [/Style]Ваш метод Generate должен возвращать:
[formatted title]
Author: [Author], Date: [Date]В вашем основном файле выведите три строки:
- Автор, доступ к которому осуществляется напрямую через Report (демонстрация того, что встраивание продвигает поле)
- Дата, доступ к которой осуществляется через имя встроенного типа (
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 через встраивание, в то время как форматер остается скрытым — вы не можете вызвать report.Format() напрямую, потому что он агрегирован с именованным полем. Это демонстрирует отношения "is-a" (является) против "has-a" (содержит) на практике.
Шпаргалка
Go поддерживает два способа включения одной структуры в другую: встраивание (embedding) и агрегацию (aggregation).
Встраивание опускает имя поля, продвигая поля и методы внутреннего типа во внешнюю структуру:
type Writer struct{}
func (w Writer) Write() string { return "writing" }
type Document struct {
Writer // встраивание — без имени поля
}
d := Document{}
d.Write() // прямой доступ к встроенному методу
Агрегация использует явное имя поля, требуя это имя для доступа к внутреннему типу:
type Document struct {
writer Writer // агрегация — именованное поле
}
d := Document{writer: Writer{}}
d.writer.Write() // необходимо использовать имя поля
Когда использовать каждый из способов:
- Встраивание: Выражает отношение «является» (is-a). Используйте, когда внешний тип должен удовлетворять интерфейсам внутреннего типа или когда прямой доступ к методам улучшает API.
- Агрегация: Выражает отношение «содержит» (has-a). Используйте, когда внутренний тип является деталью реализации, которую не следует раскрывать, или когда вам нужен явный контроль над доступом.
При встраивании вы все равно можете получить доступ к полям через имя встроенного типа, если это необходимо:
type Metadata struct {
Author string
}
type Report struct {
Metadata // встроено
}
r := Report{Metadata: Metadata{Author: "Jane"}}
r.Author // прямой доступ через продвижение
r.Metadata.Author // явный доступ через имя типа
Попробуйте сами
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