Интерфейс как контракт
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy — урок 27 из 107.
Рассматривая интерфейсы как контракты, вы сможете проектировать более качественные программы на Go. Когда функция принимает тип интерфейса, она как бы говорит: «Мне не важно, какой конкретный тип вы мне передадите, до тех пор, пока он соблюдает этот контракт».
Такой подход, основанный на контрактах, позволяет писать функции, которые фокусируются на поведении, а не на конкретных типах. Рассмотрим функцию, которой необходимо обрабатывать данные из различных источников:
type DataSource interface {
FetchData() string
}
func ProcessData(src DataSource) {
data := src.FetchData()
fmt.Println("Processing:", data)
}Функция ProcessData не знает и ей не важно, получает ли она соединение с базой данных, API-клиент или ридер файлов. Ей требуется только, чтобы входные данные могли выполнять FetchData(). Любой тип, соблюдающий этот контракт, будет работать.
type Database struct{ ConnectionString string }
func (d Database) FetchData() string { return "data from DB" }
type APIClient struct{ Endpoint string }
func (a APIClient) FetchData() string { return "data from API" }
func main() {
db := Database{ConnectionString: "localhost"}
api := APIClient{Endpoint: "https://api.example.com"}
ProcessData(db) // Работает!
ProcessData(api) // Тоже работает!
}Этот контрактный подход также делает ваш код более тестируемым. Во время тестирования вы можете создать мок-тип, который удовлетворяет интерфейсу, без необходимости использования реальных баз данных или сетевых соединений. Тестируемая функция не заметит разницы, так как контракт соблюден.
Задание
ЛегкоДавайте создадим систему уведомлений, которая демонстрирует, как интерфейсы работают в качестве контрактов. Вы создадите интерфейс Notifier, который определяет, что значит отправить уведомление, а затем реализуете несколько каналов уведомлений, которые соответствуют этому контракту.
Вы организуете свой код в двух файлах:
notifiers.go: Определите интерфейсNotifierс единственным методомNotify(message string) string. Затем создайте три различные структуры, которые удовлетворяют этому контракту:ConsoleNotifierс полемPrefixFileNotifierс полемFilenameWebhookNotifierс полемURL
Notify, который возвращает строку, описывающую, как будет отправлено уведомление.main.go: Создайте функцию с именемBroadcastAlert, которая принимает срез значенийNotifierи строку сообщения. Эта функция должна вызыватьNotifyдля каждого уведомителя и выводить результаты. Прочитайте конфигурацию из входных данных, создайте все три типа уведомителей и разошлите оповещение через них всех.
Будут предоставлены следующие входные данные:
- Строка 1: Префикс консоли
- Строка 2: Имя файла
- Строка 3: URL-адрес вебхука
- Строка 4: Сообщение об оповещении
Ваши методы Notify должны возвращать строки в следующих форматах:
- ConsoleNotifier:
[Prefix] [message] - FileNotifier:
Writing to [Filename]: [message] - WebhookNotifier:
POST [URL]: [message]
Например, при вводе ALERT:, log.txt, https://hooks.example.com и Server down! ваш вывод должен быть следующим:
ALERT: Server down!
Writing to log.txt: Server down!
POST https://hooks.example.com: Server down!Сила такого дизайна заключается в том, что ваша функция BroadcastAlert не знает и не заботится о конкретных типах уведомителей — ей требуется только то, чтобы каждый из них выполнял контракт Notifier. Позже вы сможете добавить SlackNotifier или EmailNotifier, вообще не меняя функцию рассылки.
Шпаргалка
Интерфейсы в Go выступают в роли контрактов, которые определяют необходимое поведение без указания конкретных типов. Когда функция принимает тип интерфейса, может быть использован любой тип, реализующий методы этого интерфейса.
Определите интерфейс, указав сигнатуры методов:
type DataSource interface {
FetchData() string
}Функции, принимающие интерфейсы, работают с любым типом, который удовлетворяет контракту:
func ProcessData(src DataSource) {
data := src.FetchData()
fmt.Println("Processing:", data)
}Несколько типов могут реализовывать один и тот же интерфейс:
type Database struct{ ConnectionString string }
func (d Database) FetchData() string { return "data from DB" }
type APIClient struct{ Endpoint string }
func (a APIClient) FetchData() string { return "data from API" }
func main() {
db := Database{ConnectionString: "localhost"}
api := APIClient{Endpoint: "https://api.example.com"}
ProcessData(db) // Работает!
ProcessData(api) // Тоже работает!
}Этот подход, основанный на контрактах, обеспечивает:
- Функции, которые фокусируются на поведении, а не на конкретных типах
- Лучшую тестируемость благодаря мок-реализациям
- Расширяемость без изменения существующего кода
Попробуйте сами
package main
import (
"bufio"
"fmt"
"os"
)
// BroadcastAlert отправляет сообщение через все уведомления
// TODO: Реализуйте эту функцию, чтобы:
// - Принимать срез значений Notifier и строку сообщения
// - Вызывать Notify для каждого уведомления и выводить результаты
func BroadcastAlert(notifiers []Notifier, message string) {
// TODO: Пройтись циклом по уведомлениям и вывести результат каждого уведомления
}
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Чтение префикса консоли
scanner.Scan()
prefix := scanner.Text()
// Чтение имени файла
scanner.Scan()
filename := scanner.Text()
// Чтение URL-адреса вебхука
scanner.Scan()
url := scanner.Text()
// Чтение сообщения об оповещении
scanner.Scan()
message := scanner.Text()
// TODO: Создать экземпляры всех трех типов уведомлений
// - ConsoleNotifier с префиксом
// - FileNotifier с именем файла
// - WebhookNotifier с URL-адресом
// TODO: Создать срез Notifier, содержащий все три уведомления
// TODO: Вызвать BroadcastAlert с уведомлениями и сообщением
_ = prefix
_ = filename
_ = url
_ = message
fmt.Println("TODO: Implement the solution")
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
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