Menu
Coddy logo textTech

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

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

challenge icon

Задание

Легко

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

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

  1. Author, к которому выполнен прямой доступ через Report (что демонстрирует, как embedding делает field доступным)
  2. Date, к которому выполнен доступ через имя встроенного типа (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 посредством 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
}
quiz iconПроверьте себя

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

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

Потренируйтесь самостоятельно: Онлайн-компилятор Go