Menu
Coddy logo textTech

Встраивание против агрегации

Часть раздела Объектно-ориентированное программирование путешествия по 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 чище. Используйте агрегацию, когда внутренний тип является деталью реализации, которая не должна быть открыта, или когда вам нужен явный контроль над тем, как осуществляется доступ к внутреннему типу.

challenge icon

Задание

Легко

Давайте создадим систему генерации отчетов, которая демонстрирует, когда использовать встраивание (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]

В вашем основном файле выведите три строки:

  1. Автор, доступ к которому осуществляется напрямую через Report (демонстрация того, что встраивание продвигает поле)
  2. Дата, доступ к которой осуществляется через имя встроенного типа (report.Metadata.Date)
  3. Результат вызова 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
}
quiz iconПроверьте себя

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

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