← Все этапы
04 · ПАКЕТЫ И НАДЁЖНОСТЬУрок 22 из 3323 минуты

Интерфейсы как контракты поведения

Принимаем небольшое необходимое поведение и подключаем разные типы без общей иерархии.

Требовать поведение, а не конкретную структуру

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

Интерфейс полезнее всего определять рядом с кодом, который его принимает, и оставлять небольшим: потребителю следует требовать только нужное ему действие. В параметрах используют сам интерфейс, например io.Reader, а не указатель на интерфейс. Передавать указатель на конкретное значение можно, если это нужно реализации.

Пример

package main

import "fmt"

type measurer interface {
	area() float64
}

type rectangle struct {
	width  float64
	height float64
}

func (r rectangle) area() float64 {
	return r.width * r.height
}

type circle struct {
	radius float64
}

func (c circle) area() float64 {
	return 3.14 * c.radius * c.radius
}

func printArea(name string, shape measurer) {
	fmt.Printf("%s: %.2f\n", name, shape.area())
}

func main() {
	printArea("прямоугольник", rectangle{width: 4, height: 2})
	printArea("круг", circle{radius: 1})
}

Функция printArea знает только метод area, поэтому обе формы подходят без регистрации или объявления наследования. Оба метода имеют получатель-значение, значит и значение, и указатель на соответствующую структуру имеют этот метод в своём наборе методов. Программа печатает «прямоугольник: 8.00» и «круг: 3.14».

Если метод объявлен с получателем-указателем, например func (r *rectangle) resize(...), интерфейс с этим методом реализует только *rectangle, но не rectangle. Вызов метода на адресуемом значении иногда автоматически берёт его адрес, однако это правило вызова не меняет набор методов типа. Поэтому интерфейсное значение в таком случае заполняют указателем: var item resizer = &shape.

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

Практика

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

Определи интерфейс notifier с методом notify() string и функцию send(n notifier), которая печатает результат этого метода. Реализуй его для типа email со строковым адресом и для типа push с названием устройства. Пусть notify возвращает строки «письмо отправлено: адрес» и «push отправлен: устройство». Используй адрес user@example.test и устройство Телефон; сначала передай в send email, затем push.

Проверка результата

Как понять, что получилось

Программа печатает строки «письмо отправлено: user@example.test» и «push отправлен: Телефон» в таком порядке, используя один вызов функции send для каждого значения. Интерфейс содержит только notify() string; функция принимает notifier, не указатель на интерфейс.

Интерфейсы как контракты поведения | Go | WebSchool · Go