Advertisement
  1. Code
  2. Go

Как определить и реализовать интерфейс Go

by
Read Time:8 minsLanguages:

Russian (Pусский) translation by Ilya Nikov (you can also view the original English article)

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

В этом руководстве вы узнаете, что такое интерфейс, как Go воспринимает интерфейсы, как реализовать интерфейс в Go и, наконец, ограничения интерфейсов и контрактов.

Что такое интерфейс Go?

Интерфейс Go - это тип, который состоит из набора сигнатур методов. Вот пример интерфейса Go:

Интерфейс Serializable имеет два метода. Метод Serialize() не принимает аргументов и возвращает строку и ошибку, а метод Deserialize() принимает строку и возвращает ошибку. Интерфейс Serializable, вероятно, знаком вам по другим языкам, и вы можете догадаться, что метод Serialize() возвращает сериализованную версию целевого объекта, которую можно восстановить, вызвав Deserialize() и передав результат первоначального вызова Serialize().

Обратите внимание, что вам не нужно указывать ключевое слово «func» в начале каждого объявления метода. Go уже знает, что интерфейс может содержать только методы и не нуждается в какой-либо помощи, если вы скажете, что это "func".

Рекомендации по интерфейсам Go

Интерфейсы Go - лучший способ создать основу вашей программы. Объекты должны взаимодействовать друг с другом через интерфейсы, а не через конкретные объекты. Это означает, что вы должны построить объектную модель для вашей программы, которая состоит только из интерфейсов и базовых типов или объектов данных (структур, членами которых являются базовые типы или другие объекты данных). Вот некоторые из лучших практик, которые вы должны использовать при работе с интерфейсами.

Четкие намерения

Важно, чтобы цель каждого метода и последовательность вызовов были ясны и четко определены как для вызывающих, так и для разработчиков. Для этого в Go нет поддержки на уровне языка. Я расскажу об этом подробнее в разделе «Интерфейс против контракта» позже.

Внедрение зависимости

Внедрение зависимости означает, что объект, который взаимодействует с другим объектом через интерфейс, получит интерфейс извне в качестве аргумента функции или метода и не будет создавать объект (или вызывать функцию, которая возвращает конкретный объект). Обратите внимание, что этот принцип применим и к автономным функциям, а не только к объектам. Функция должна получать все свои зависимости как интерфейсы. Например:

Теперь вы вызываете функцию foo() с различными реализациями SomeInterface, и она будет работать со всеми из них.

Фабрики

Очевидно, кто-то должен создавать конкретные объекты. Это работа специализированных объектов. Фабрики используются в двух ситуациях:

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

Часто полезно предоставлять динамические фабричные интерфейсы для объектов, чтобы поддерживать шаблон взаимодействия только для интерфейса. В следующем примере я определяю интерфейс Widget и интерфейс WidgetFactory, который возвращает интерфейс Widget из метода CreateWidget().

Функция PerformMainLogic() получает интерфейс WidgetFactory от своего вызывающего. Теперь она может динамически создавать новый виджет на основе его спецификации виджета и вызывает его метод Widgetize(), ничего не зная о его конкретном типе (какая структура реализует интерфейс).

Тестируемость

Тестируемость является одним из наиболее важных методов правильной разработки программного обеспечения. Интерфейсы Go - лучший механизм поддержки тестируемости в программах Go. Чтобы тщательно протестировать функцию или метод, вам необходимо контролировать и/или измерять все входы, выходы и побочные эффекты для тестируемой функции.

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

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

Вместо импорта time и непосредственного вызова time.Now(), вы можете передать интерфейс с методом Now(), который в производственном процессе будет реализован путем перенаправления в time.Now(), но во время тестирования будет реализован объект, который возвращает фиксированное время для замораживания тестовой среды.

Использование интерфейса Go

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

Реализация интерфейса Go

Интерфейсы Go могут быть реализованы как методы на структурах. Рассмотрим следующий интерфейс:

Вот две конкретные реализации интерфейса Shape:

Квадрат и прямоугольник осуществляют вычисления по-разному в зависимости от их полей и геометрических свойств. В следующем примере кода демонстрируется, как заполнить фрагмент интерфейса Shape конкретными объектами, которые реализуют интерфейс, а затем выполнить итерацию по фрагменту и вызвать метод GetArea() для каждой фигуры для вычисления общей площади всех фигур.

Базовая реализация

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

Вы можете получить аналогичный эффект, встроив структуру. Давайте определим структуру Cache, которая может хранить значения предыдущих вычислений. Когда значение извлекается из кейса, оно также выводит на экран «попадание в кэш», а когда значение не в кейсе, оно печатает «пропущенный кеш» и возвращает -1 (допустимые значения - целые числа без знака).

Теперь я вставлю этот кеш в квадрат и прямоугольник. Обратите внимание, что реализация GetPerimeter() и GetArea() теперь сначала проверяет кэш и вычисляет значение, только если его нет в кэше.

Наконец, функция main() дважды вычисляет общую площадь, чтобы увидеть эффект кэширования.

Вот вывод:

Интерфейс против Контракта

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

Чтобы выйти за пределы этого базового уровня, вам нужен контракт. Контракт для объекта точно определяет, что делает каждый метод, какие побочные эффекты выполняются, и каково состояние объекта в каждый момент времени. Контракт всегда существует. Единственный вопрос, является ли это явным или неявным. Что касается внешних API, контракты имеют решающее значение.

Заключение

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

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.