Утиная типизация в 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 дает вам гибкость динамических языков, сохраняя при этом типобезопасность на этапе компиляции. Компилятор проверяет, что типы действительно имеют необходимые методы, прежде чем разрешить их использование в качестве значений интерфейса.
Задание
ЛегкоДавайте создадим систему обмена сообщениями, которая демонстрирует «утиную типизацию» (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
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
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