Преимущества интерфейсов в GO

Преимущества интерфейсов в GO


В языке GO интерфейсы отличаются от других популярных языков программирования, таких как Java, C++, PHP. Они имеют некоторые преимущества с точки зрения дизайна. В этой статье я постараюсь объяснить почему.
Я расскажу о преимуществах, приведу примеры и рассмотрю некоторые вопросы, которые могут возникнуть при использовании интерфейсов.


В чем особенность интерфейсов в GO?


Под особенность я буду иметь в виду утиную типизацию. Она также присутствует в других языках, таких как python, js, ruby. Но в отличие от них, она сочетается со строгой типизацией языка, что несет за собой некоторые плюсы. Утиная типизация в GO больше схожа с TypeScript. Но в этой статье я не буду разбирать другие языки.
Коротко опишу особенность. В большинстве языков вы описываете один интерфейс, и реализуете их в других местах явно, указывая, что вы реализуете именно их.
Например так в PHP:


class Human implements Walkable
{
…
}

class Mountain
{
    public function walkAround(Walkable $walkable) {...}
}

А потом используете так:


$human = new Human();
$mountain = new Mountain();
$mountain.walkAround($human);

Но в GO это не так. Вам не нужно указывать явно, что вы реализуете его в своей структуре. Если у структуры есть все функции и они имеют одинаковую сигнатуру с интерфейсом, то она уже его реализует.


Какие преимущество дает эта особенность?


Приватный интерфейс


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


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


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


package auth

import (
   "gitlab.com/excercice_detection/backend"
)

type userRepository interface {
   FindUserByEmail(email string) (backend.User, error)
   AddUser(backend.User) (userID int, err error)
   AddToken(userID int, token string) error
   TokenExists(userID int, token string) bool
}

// Auth сервис авторизации
type Auth struct {
   repository userRepository
   logger     backend.Logger
}

// NewAuth создает объект авторизации
func NewAuth(repository userRepository, logger backend.Logger) *Auth {
   return &Auth{repository, logger}
}

// Autentificate Проверяет существование токена пользователя
func (auth Auth) Autentificate(userID int, token string) bool {
   return auth.repository.TokenExists(userID, token)
}

Для примера я показал как используется один из методов, на самом деле они используются все.


В главном методе main создается и используется объект авторизации:


package main

import (
   "gitlab.com/excercice_detection/backend/auth"
   "gitlab.com/excercice_detection/backend/mysql"
)

func main() {
    logger := newLogger()
    userRepository := mysql.NewUserRepository(logger)
    err := userRepository.Connect()
    authService := auth.NewAuth(userRepository, logger)
...

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


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


Если вы передадите объект, который не реализует нужный интерфейс, вы получите ошибку на этапе компиляции.


Такой интерфейс удобно расширять


Если вам в будущем пригодятся другие методы из репозитория, вы можете просто добавить их в этот приватный интерфейс. Он немного усложнится, но только внутри этого модуля. Вам не нужно докидывать их в неком общем интерфейсе, а затем описывать методы везде, где он реализуется.
Это позволяет не вводить множества слоев абстракции вначале, а лишь добавлять необходимые функции в процессе развития системы.
При этом желательно не делать интерфейсы широкими. Т.е. не нужно добавлять в них множество методов. Лучше оставлять их узкими. Это позволит не путаться в реализациях методов и проще воспринимать сам код. Если интерфейс становится большим, это звоночек, что: либо пора выделить новый интерфейс, либо изменить сами методы, чтобы интерфейс оказался проще.


Тесты


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


Пример мока:


type userRepositoryMock struct {
   user         backend.User
   findUserErr  error
   addUserError error
   addUserID    int
   addTokenErr  error
   tokenExists  bool
}

func (repository userRepositoryMock) FindUserByEmail(email string) (backend.User, error) {
   return repository.user, repository.findUserErr
}

func (repository userRepositoryMock) AddUser(backend.User) (userID int, err error) {
   return repository.addUserID, repository.addUserError
}

func (repository userRepositoryMock) AddToken(userID int, token string) error {
   return repository.addTokenErr
}

func (repository userRepositoryMock) TokenExists(userID int, token string) bool {
   return repository.tokenExists
}

Далее, в тестах, userRepositoryMock можно подсунуть вместо обычного userRepositorу, подставляя нужные значения, которые должна вернуть функции.


Методы интерфейса могут путаться между разными реализациями


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


Как понять, кто на самом деле реализует метод, используемый из интерфейса?


Кажется, что закрываясь интерфейсом и не указывая явно кто его реализует, мы теряем знание о том, как на самом деле работает нужная функция. Но, поскольку GO является строго типизированным языком, то узнать кто реализует метод достаточно просто. Например IDE GoLand умеет так делать.


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


Как найти места, где используются реализованные методы, если они закрыты интерфейсом?


Ответ тот же самый. Это тоже легко поддается статическому анализу. Если ваша IDE не может найти реализации, то это её недостаток, а не интерфейсов. Если IDE развивается, то в ближайшем будущем эта функция должна появиться.


Заключение


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


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

Комментарии 18

    +7
    Эту, не уникальную для Go, особенность называют «Утиной типизацией»
      –6
      Спасибо за информацию. Об этом я не знал. С этим впервые столкнулся в GO, поэтому взял именно его для рассмотрения.
        +1

        А еще есть более умный термин структурная типизация.

        +2
        Вроде в Typescript интерфейсы работают аналогично.
          +8
          Если ваша IDE не может найти реализации, то это её недостаток, а не интерфейсов.

          Удобный аргумент :) Возьму себе на вооружение.

            +2

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

              +5
              Язык GO кажется подходящим инструментом для развития больших проектов.

              Ключевое слово "кажется"?!

                –2

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


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

                Если писать на Go идеоматично, то такая проблема не должна возникать часто, потому что в Go принято интерфейсы делать максимально узкими.

                  +4

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


                  Интерфейсы больше полезны для документации кода и по ним удобно читать скелет модуля/системы.


                  Возвращаясь к аргументу про ИСР — если ваша ИСР не умеет экстрактить/рефакторить интерфейсы, то это её проблемы ))

                    0
                    Согласен что возможно большое пересечение методов. Возможно дополню статью.
                    Но при этом никто же не мешает использовать общие интерфейсы, если они действительно хорошо отделяются. Важно чтобы интерфейсы были небольшими, тогда проблем с большим количеством пересечений быть не должно.
                    Как вы документируете модуль через интерфейсы? Модуль — это же про реализацию. Разные модули могут реализовывать один и тот же интерфейс. У интерфейса немного своя документация. У модуля документация скорее про нюансы реализации.
                    0
                    Так и не понял в чем выгода использования именно приватного интерфейса
                      0
                      Основное преимущество что нет лишних зависимостей. Мы не зависим от внешнего интерфейса, который может измениться, или может возникнуть потребность его изменить для других модулей.
                      В итоге код получается немного проще.
                        0
                        Комментарием выше уже все сказано, т.ч. просто оставлю ссылку на рекомендации разработчиков языка.
                        github.com/golang/go/wiki/CodeReviewComments#interfaces
                        0

                        Этот текст — курсовая работа студента?

                          0

                          Нет. Почему вы так думаете?

                            +1

                            Стиль статьи очень походит на таковой в курсовых работах.
                            Бросаются в глаза такие вещи, как: 1) утверждения, выходящие за пределы темы (концовка статьи, заключение); 2) голословные утверждения. Например, Вы утверждаете, что интерфейсы Go лучше таковых в других языках, но ничего не пишете, о том, а каково оно у них, да и о каких языках речь-то? Примеры хороши, но они не доказывают, что интерфейсы Go лучше, так как сравнить их не с чем. 3) наличие спорных утверждений, например, об IDE. Это отдельная тема для обсуждения.
                            Вероятно, что все эти вещи и повлияли на то, что статью заминусовали.

                              –1
                              Спасибо. Учту это в своих будущих статьях.
                              1) В концовке я написал, что он подходит для больших проектов говоря об этом в контексте использования интерфейсов, поэтому я указал, что
                              Язык GO кажется подходящим инструментом
                              а не написал, что он точно является подходящим.
                              2) Я старался показать именно сильные стороны, показывая разницу с самым распространенным способом работы с интерфейсами в популярных языках как Java, C++, PHP. Но видимо да, нужно было привести примеры с других языков, чтобы было понятнее.
                              Видимо, нужно писать подробнее, раскрывая больше мыслей. Чтобы не было таких недопониманий и негативных реакций. Это моя первая статья на хабре.
                                0

                                Пожалуйста! В принципе автор может дорабатывать и исправлять свою статью. В этом нет ничего плохого, напротив. Удачи!

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое