Полиморфизм через интерфейсы
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy — урок 41 из 107.
Полиморфизм позволяет единообразно обрабатывать различные типы через общий интерфейс. В Go это достигается исключительно с помощью интерфейсов, без наследования или иерархий классов.
Когда функция принимает тип интерфейса в качестве параметра, в неё можно передать любой конкретный тип, реализующий этот интерфейс. Функции не нужно знать конкретный тип — её интересует только поведение, определённое интерфейсом:
type Speaker interface {
Speak() string
}
type Dog struct{ Name string }
func (d Dog) Speak() string { return "Woof!" }
type Cat struct{ Name string }
func (c Cat) Speak() string { return "Meow!" }
func MakeSound(s Speaker) {
fmt.Println(s.Speak())
}
Теперь MakeSound работает с любым типом, у которого есть метод Speak():
func main() {
dog := Dog{Name: "Rex"}
cat := Cat{Name: "Whiskers"}
MakeSound(dog) // Гав!
MakeSound(cat) // Мяу!
}
Один и тот же вызов функции приводит к разному поведению в зависимости от фактически переданного типа. Это полиморфизм в действии. Функция MakeSound написана один раз, но работает с неограниченным количеством типов, если они удовлетворяют интерфейсу Speaker.
Этот подход делает ваш код гибким и расширяемым. Добавление нового типа, который «говорит», не требует изменений в существующих функциях — достаточно реализовать интерфейс, и всё заработает автоматически.
Задание
ЛегкоДавайте создадим систему описания транспортных средств, которая демонстрирует полиморфизм в действии. Вы создадите различные типы транспортных средств, которые разделяют общее поведение через интерфейс, а затем напишете одну функцию, которая работает с любым транспортным средством.
Вы организуете свой код в двух файлах:
vehicles.go: Определите интерфейсDescriber, который требует методDescribe() string. Затем создайте три типа транспортных средств, каждый из которых реализует этот интерфейс по-своему:Carс полямиBrandиModel— его методDescribe()возвращает"Car: [Brand] [Model]"Motorcycleс полямиBrandиEngineCC(int) — его методDescribe()возвращает"Motorcycle: [Brand] [EngineCC]cc"Bicycleс полемType(например, "Mountain" или "Road") — его методDescribe()возвращает"Bicycle: [Type]"
main.go: Создайте функцию с именемPrintDescription, которая принимает любойDescriberи выводит результат вызоваDescribe(). Считайте данные о транспортных средствах из входного потока, создайте по одному экземпляру каждого типа и передайте каждый из них вPrintDescription, чтобы продемонстрировать, что одна и та же функция работает со всеми тремя различными типами.
Будут предоставлены следующие входные данные:
- Строка 1: Марка автомобиля (Car brand)
- Строка 2: Модель автомобиля (Car model)
- Строка 3: Марка мотоцикла (Motorcycle brand)
- Строка 4: Объем двигателя мотоцикла (Motorcycle engine CC, целое число)
- Строка 5: Тип велосипеда (Bicycle type)
Например, для входных данных Toyota, Camry, Honda, 600 и Mountain ваш вывод должен быть следующим:
Car: Toyota Camry
Motorcycle: Honda 600cc
Bicycle: MountainОбратите внимание, что функции PrintDescription не нужно знать, получает ли она Car, Motorcycle или Bicycle — она просто вызывает Describe(), и каждый тип отвечает своим уникальным выводом. Это и есть полиморфизм: одна функция, несколько вариантов поведения.
Шпаргалка
Полиморфизм позволяет единообразно работать с различными типами через общий интерфейс. В Go это достигается с помощью интерфейсов без использования наследования или иерархий классов.
Когда функция принимает тип интерфейса в качестве параметра, в неё можно передать любой конкретный тип, реализующий этот интерфейс:
type Speaker interface {
Speak() string
}
type Dog struct{ Name string }
func (d Dog) Speak() string { return "Woof!" }
type Cat struct{ Name string }
func (c Cat) Speak() string { return "Meow!" }
func MakeSound(s Speaker) {
fmt.Println(s.Speak())
}
Одна и та же функция работает с разными типами:
func main() {
dog := Dog{Name: "Rex"}
cat := Cat{Name: "Whiskers"}
MakeSound(dog) // Woof!
MakeSound(cat) // Meow!
}
Функция пишется один раз, но работает с неограниченным количеством типов, пока они соответствуют интерфейсу. Добавление новых типов не требует изменений в существующих функциях — достаточно просто реализовать интерфейс.
Попробуйте сами
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
// TODO: Создайте функцию с именем PrintDescription, которая принимает любой Describer
// и выводит результат вызова Describe()
func main() {
reader := bufio.NewReader(os.Stdin)
// Чтение данных автомобиля
carBrand, _ := reader.ReadString('\n')
carBrand = strings.TrimSpace(carBrand)
carModel, _ := reader.ReadString('\n')
carModel = strings.TrimSpace(carModel)
// Чтение данных мотоцикла
motoBrand, _ := reader.ReadString('\n')
motoBrand = strings.TrimSpace(motoBrand)
motoEngineStr, _ := reader.ReadString('\n')
motoEngineStr = strings.TrimSpace(motoEngineStr)
motoEngine, _ := strconv.Atoi(motoEngineStr)
// Чтение данных велосипеда
bicycleType, _ := reader.ReadString('\n')
bicycleType = strings.TrimSpace(bicycleType)
// TODO: Создайте Car, Motorcycle и Bicycle, используя входные значения
// TODO: Вызовите PrintDescription для каждого транспортного средства, чтобы продемонстрировать полиморфизм
fmt.Println("TODO: Print vehicle descriptions")
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
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