Menu
Coddy logo textTech

Полиморфные коллекции

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

challenge icon

Задание

Легко

Давайте создадим систему управления задачами, которая обрабатывает различные типы задач через единый интерфейс. Вы создадите различные типы задач и обработаете их все вместе в полиморфной коллекции.

Вы организуете свой код в трех файлах:

  • 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] pts
    • Documentation с полем 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 со срезом задач
}
quiz iconПроверьте себя

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

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