Неявная реализация
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy — урок 26 из 107.
Во многих объектно-ориентированных языках, таких как Java или C#, вы должны явно объявить, что тип реализует интерфейс, используя такие ключевые слова, как implements. Go использует другой подход: реализация интерфейса является неявной.
Тип удовлетворяет интерфейсу, просто реализуя все его методы. Нет необходимости объявлять эту связь где-либо. Компилятор определяет это автоматически.
type Writer interface {
Write(data string) int
}
type FileWriter struct {
Filename string
}
// FileWriter неявно реализует Writer
func (f FileWriter) Write(data string) int {
fmt.Println("Writing to", f.Filename)
return len(data)
}
Обратите внимание, что в FileWriter отсутствует объявление implements Writer. Поскольку FileWriter имеет метод Write(data string) int, соответствующий сигнатуре интерфейса, он автоматически удовлетворяет интерфейсу Writer.
Такой дизайн имеет важное последствие: вы можете определять интерфейсы уже после того, как типы были созданы. Если в сторонней библиотеке есть тип с нужными вам методами, вы можете создать интерфейс, которому он уже соответствует, не изменяя исходный код.
func Save(w Writer, content string) {
w.Write(content)
}
func main() {
fw := FileWriter{Filename: "data.txt"}
Save(fw, "Hello!") // Работает, так как FileWriter удовлетворяет Writer
}
Неявная реализация делает код на Go слабосвязанным и гибким. Типам не нужно знать об интерфейсах заранее, что упрощает построение систем из независимых частей.
Задание
ЛегкоДавайте создадим систему доставки сообщений, которая демонстрирует, как типы неявно удовлетворяют интерфейсам в Go. Вы создадите различные типы мессенджеров, которые удовлетворяют одному и тому же интерфейсу — без явного объявления этой связи.
Вы организуете свой код в двух файлах:
messengers.go: Определите интерфейсMessengerс единственным методомSend(message string) string. Затем создайте две структуры, которые будут неявно удовлетворять этому интерфейсу:EmailMessengerс полемAddress(string)SMSMessengerс полемPhoneNumber(string)
Send, который возвращает строку, описывающую доставку. Ни одна из структур не должна явно объявлять, что она реализуетMessenger— Go определит это автоматически на основе сигнатур методов.main.go: Создайте функцию с именемDeliverMessage, которая принимает любойMessengerи строку сообщения, а затем возвращает результат вызоваSend. Считайте контактную информацию из входных данных, создайте оба типа мессенджеров и используйте свою функцию для доставки сообщений через каждый из них.
Будут предоставлены следующие входные данные:
- Строка 1: Email адрес
- Строка 2: Номер телефона
- Строка 3: Сообщение для отправки
Ваши методы Send должны возвращать строки в следующих форматах:
- EmailMessenger:
Email to [Address]: [message] - SMSMessenger:
SMS to [PhoneNumber]: [message]
Например, при вводе alice@example.com, 555-1234 и Hello!, ваш вывод должен быть:
Email to alice@example.com: Hello!
SMS to 555-1234: Hello!Ключевая идея здесь заключается в том, что ваша функция DeliverMessage принимает любой Messenger, и как EmailMessenger, так и SMSMessenger удовлетворяют этому интерфейсу просто за счет наличия соответствующего метода Send. Здесь нигде нет ключевого слова implements — неявная реализация в Go берет все на себя.
Шпаргалка
В Go реализация интерфейсов является неявной. Тип удовлетворяет интерфейсу, просто реализуя все его методы — никакого явного объявления не требуется.
type Writer interface {
Write(data string) int
}
type FileWriter struct {
Filename string
}
// FileWriter неявно реализует Writer
func (f FileWriter) Write(data string) int {
fmt.Println("Writing to", f.Filename)
return len(data)
}
Здесь нет объявления implements Writer. Поскольку у FileWriter есть метод Write(data string) int, соответствующий сигнатуре интерфейса, он автоматически удовлетворяет интерфейсу Writer.
Вы можете использовать любой тип, который удовлетворяет интерфейсу, при этом самому типу не обязательно «знать» об этом интерфейсе:
func Save(w Writer, content string) {
w.Write(content)
}
func main() {
fw := FileWriter{Filename: "data.txt"}
Save(fw, "Hello!") // Работает, так как FileWriter удовлетворяет Writer
}
Такой подход позволяет определять интерфейсы уже после создания типов, что делает код слабосвязанным и гибким.
Попробуйте сами
package main
import (
"bufio"
"fmt"
"os"
)
// TODO: Создайте функцию DeliverMessage, которая принимает любой Messenger
// и строку сообщения, затем возвращает результат вызова Send
func main() {
reader := bufio.NewReader(os.Stdin)
// Чтение адреса электронной почты
email, _ := reader.ReadString('\n')
email = email[:len(email)-1]
// Чтение номера телефона
phone, _ := reader.ReadString('\n')
phone = phone[:len(phone)-1]
// Чтение сообщения
message, _ := reader.ReadString('\n')
if len(message) > 0 && message[len(message)-1] == '\n' {
message = message[:len(message)-1]
}
// TODO: Создайте EmailMessenger с адресом электронной почты
// TODO: Создайте SMSMessenger с номером телефона
// TODO: Используйте DeliverMessage для отправки сообщения через каждый мессенджер
// и выведите результаты
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
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