Menu
Coddy logo textTech

Интерфейс как контракт

Часть раздела Объектно-ориентированное программирование путешествия по 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)  // Тоже работает!
}

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

challenge icon

Задание

Легко

Давайте создадим систему уведомлений, которая демонстрирует, как интерфейсы работают в качестве контрактов. Вы создадите интерфейс Notifier, который определяет, что значит отправить уведомление, а затем реализуете несколько каналов уведомлений, которые соответствуют этому контракту.

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

  • notifiers.go: Определите интерфейс Notifier с единственным методом Notify(message string) string. Затем создайте три различные структуры, которые удовлетворяют этому контракту:
    • ConsoleNotifier с полем Prefix
    • FileNotifier с полем Filename
    • WebhookNotifier с полем 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")
}
quiz iconПроверьте себя

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

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