Полиморфные коллекции
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy — урок 44 из 107.
Одним из наиболее мощных применений интерфейсов является хранение различных типов в одной коллекции. Срез типа интерфейса может содержать любое значение, удовлетворяющее этому интерфейсу, что позволяет группировать связанные, но разные типы вместе.
Рассмотрим сценарий, в котором вам необходимо управлять различными фигурами. Вместо отдельных срезов для каждого типа вы можете использовать один срез интерфейсного типа:
type Shape interface {
Area() float64
}
type Circle struct{ Radius float64 }
func (c Circle) Area() float64 { return 3.14159 * c.Radius * c.Radius }
type Rectangle struct{ Width, Height float64 }
func (r Rectangle) Area() float64 { return r.Width * r.Height }
func main() {
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Width: 4, Height: 3},
Circle{Radius: 2},
}
for _, s := range shapes {
fmt.Printf("Area: %.2f\n", s.Area())
}
}
Срез shapes содержит как круги, так и прямоугольники. При итерации каждый элемент отвечает на Area() в соответствии со своим фактическим типом. Это полиморфизм, примененный к коллекциям — один и тот же цикл единообразно обрабатывает все типы фигур.
Полиморфные коллекции необходимы при создании систем, обрабатывающих различные элементы: система уведомлений, отправляющая электронные письма и SMS-сообщения, игра, обновляющая различные типы врагов, или обработчик документов, работающий с несколькими форматами файлов. Коллекции не важны конкретные типы — важно лишь то, чтобы каждый элемент обеспечивал требуемое поведение.
Задание
ЛегкоДавайте создадим систему управления задачами, которая обрабатывает различные типы задач через единый интерфейс. Вы создадите различные типы задач и обработаете их все вместе в полиморфной коллекции.
Вы организуете свой код в трех файлах:
task.go: Определите интерфейсTask, который требует методSummary() string. Этот интерфейс будет общим контрактом, который должны выполнять все типы задач.types.go: Создайте три различных типа задач, каждый из которых реализует интерфейсTask:BugFixс полямиID(string) иSeverity(string) — его методSummary()возвращаетBug #[ID] ([Severity])Featureс полямиName(string) иPoints(int) — его методSummary()возвращаетFeature: [Name] - [Points] ptsDocumentationс полемTopic(string) — его методSummary()возвращаетDocs: [Topic]
main.go: Создайте функцию с именемPrintBacklog, которая принимает слайсTaskи выводит описание каждой задачи на отдельной строке. Считайте данные о задачах из входного потока, создайте по одному экземпляру каждого типа задач, соберите их все в один слайс[]Taskи передайте его вPrintBacklog.
Будут предоставлены следующие входные данные:
- Строка 1: Bug ID
- Строка 2: Bug severity
- Строка 3: Feature name
- Строка 4: Feature points (целое число)
- Строка 5: Documentation topic
Например, для входных данных 1042, critical, Dark Mode, 8 и API Reference ваш вывод должен быть следующим:
Bug #1042 (critical)
Feature: Dark Mode - 8 pts
Docs: API ReferenceПреимущество здесь в том, что PrintBacklog не требует отдельной логики для багов, фич или документации — она просто итерируется по слайсу и вызывает Summary() для каждого элемента. Каждый тип задачи отвечает в своем уникальном формате, демонстрируя, как полиморфные коллекции позволяют единообразно обрабатывать различные типы.
Шпаргалка
Срез типа интерфейса может содержать любое значение, удовлетворяющее этому интерфейсу, что позволяет создавать полиморфные коллекции:
type Shape interface {
Area() float64
}
type Circle struct{ Radius float64 }
func (c Circle) Area() float64 { return 3.14159 * c.Radius * c.Radius }
type Rectangle struct{ Width, Height float64 }
func (r Rectangle) Area() float64 { return r.Width * r.Height }
func main() {
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Width: 4, Height: 3},
Circle{Radius: 2},
}
for _, s := range shapes {
fmt.Printf("Area: %.2f\n", s.Area())
}
}
Срез shapes содержит различные типы (Circle и Rectangle), которые реализуют интерфейс Shape. При итерации каждый элемент отвечает на вызов Area() в соответствии со своим фактическим типом — это полиморфизм, примененный к коллекциям.
Этот паттерн полезен для единообразной обработки различных элементов: систем уведомлений, обрабатывающих разные типы сообщений, игр, обновляющих различные типы врагов, или процессоров документов, работающих с несколькими форматами файлов.
Попробуйте сами
package main
import (
"fmt"
)
// TODO: Создайте функцию PrintBacklog, которая принимает срез Task
// и выводит описание каждой задачи на отдельной строке
func main() {
// Чтение входных данных
var bugID string
var bugSeverity string
var featureName string
var featurePoints int
var docTopic string
fmt.Scanln(&bugID)
fmt.Scanln(&bugSeverity)
fmt.Scanln(&featureName)
fmt.Scanln(&featurePoints)
fmt.Scanln(&docTopic)
// TODO: Создайте по одному экземпляру каждого типа задач (BugFix, Feature, Documentation)
// TODO: Соберите все задачи в один срез []Task
// TODO: Вызовите PrintBacklog со срезом задач
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
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