Menu
Coddy logo textTech

Утиная типизация в Go

Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy — урок 42 из 107.

Термин утиная типизация происходит от поговорки: «Если оно ходит как утка и крякает как утка, то это утка». В Go это означает, что типу не нужно явно объявлять о том, что он реализует интерфейс — ему просто необходимо иметь соответствующие методы.

Рассмотрим этот интерфейс и два совершенно не связанных типа:

type Quacker interface {
    Quack() string
}

type Duck struct{}
func (d Duck) Quack() string { return "Quack!" }

type Robot struct{}
func (r Robot) Quack() string { return "Beep-quack!" }

Ни Duck, ни Robot не упоминают Quacker нигде в своих определениях. Тем не менее, оба они удовлетворяют интерфейсу просто потому, что у них есть метод Quack() с правильной сигнатурой:

func MakeItQuack(q Quacker) {
    fmt.Println(q.Quack())
}

func main() {
    MakeItQuack(Duck{})   // Quack!
    MakeItQuack(Robot{})  // Beep-quack!
}

Это неявное удовлетворение интерфейсов — мощный механизм, так как он позволяет определять интерфейсы уже после того, как типы созданы. Вы можете создать интерфейс, соответствующий типам из внешних пакетов, не изменяя их исходный код. Типам не обязательно знать о вашем интерфейсе — им достаточно вести себя правильно.

Утиная типизация в Go дает вам гибкость динамических языков, сохраняя при этом типобезопасность на этапе компиляции. Компилятор проверяет, что типы действительно имеют необходимые методы, прежде чем разрешить их использование в качестве значений интерфейса.

challenge icon

Задание

Легко

Давайте создадим систему обмена сообщениями, которая демонстрирует «утиную типизацию» (duck typing) в действии. Вы создадите совершенно не связанные между собой типы, которые могут отправлять сообщения через общий интерфейс — при этом ни один из них не будет явно объявлять о его реализации.

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

  • messengers.go: Создайте три не связанные структуры, которые имеют одинаковое поведение:
    • Phone с полем Number — его метод SendMessage(text string) string возвращает "SMS to [Number]: [text]"
    • Computer с полем Email — его метод SendMessage(text string) string возвращает "Email to [Email]: [text]"
    • Pigeon с полем Name — его метод SendMessage(text string) string возвращает "[Name] carries: [text]"
    Обратите внимание, что ни один из этих типов не упоминает какой-либо интерфейс — у них просто совпадает сигнатура метода.
  • sender.go: Определите интерфейс Messenger, который требует метод SendMessage(text string) string. Создайте функцию с именем Broadcast, которая принимает слайс Messenger и строку сообщения, а затем возвращает слайс строк, содержащий результат вызова SendMessage для каждого мессенджера.
  • main.go: Считайте данные из входного потока, создайте по одному экземпляру каждого типа мессенджера, соберите их в слайс Messenger и используйте Broadcast для отправки сообщения через них всех. Выведите каждый результат на новой строке.

Будут предоставлены следующие входные данные:

  • Строка 1: Номер телефона
  • Строка 2: Адрес электронной почты
  • Строка 3: Имя голубя
  • Строка 4: Сообщение для рассылки

Например, для входных данных 555-1234, alice@mail.com, Winston и Hello World ваш вывод должен быть следующим:

SMS to 555-1234: Hello World
Email to alice@mail.com: Hello World
Winston carries: Hello World

Ключевая идея здесь заключается в том, что Phone, Computer и Pigeon не имеют ничего общего — у них нет общего базового типа и они явно не реализуют Messenger. Тем не менее, поскольку все они «крякают» одинаково (имеют соответствующий метод), Go позволяет использовать их все как значения типа Messenger. Это и есть «утиная типизация» в действии.

Шпаргалка

В Go **утиная типизация** (duck typing) означает, что типу не нужно явно объявлять о реализации интерфейса — ему достаточно иметь необходимые методы с соответствующими сигнатурами.

Тип неявно удовлетворяет интерфейсу, если он обладает всеми методами, которые требует интерфейс:

type Quacker interface {
    Quack() string
}

type Duck struct{}
func (d Duck) Quack() string { return "Quack!" }

type Robot struct{}
func (r Robot) Quack() string { return "Beep-quack!" }

И Duck, и Robot удовлетворяют интерфейсу Quacker, не упоминая об этом в своих определениях:

func MakeItQuack(q Quacker) {
    fmt.Println(q.Quack())
}

func main() {
    MakeItQuack(Duck{})   // Quack!
    MakeItQuack(Robot{})  // Beep-quack!
}

Такая неявная реализация позволяет определять интерфейсы уже после того, как типы созданы, и даже создавать интерфейсы, соответствующие типам из внешних пакетов, не изменяя их исходный код. Компилятор на этапе компиляции проверяет наличие у типов необходимых методов, обеспечивая типобезопасность при сохранении гибкости.

Попробуйте сами

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	
	// Считать номер телефона
	scanner.Scan()
	phoneNumber := scanner.Text()
	
	// Считать адрес электронной почты
	scanner.Scan()
	email := scanner.Text()
	
	// Считать имя голубя
	scanner.Scan()
	pigeonName := scanner.Text()
	
	// Считать сообщение для рассылки
	scanner.Scan()
	message := scanner.Text()
	
	// TODO: Создать по одному экземпляру каждого типа мессенджера (Phone, Computer, Pigeon)
	
	// TODO: Собрать все мессенджеры в слайс Messenger
	
	// TODO: Использовать Broadcast для отправки сообщения через все мессенджеры
	
	// TODO: Вывести каждый результат на новой строке
	
	// Заглушка для использования переменных (удалить при реализации)
	_ = phoneNumber
	_ = email
	_ = pigeonName
	_ = message
}
quiz iconПроверьте себя

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

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