Как стать автором
Обновить

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

Время на прочтение5 мин
Количество просмотров7.6K

Преимущества интерфейсов в 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 с её строгой типизацией, быстрой компиляцией и своей философией, которая заметно отличается от многих других языков.

Теги:
Хабы:
-6
Комментарии18

Публикации

Истории

Работа

Go разработчик
130 вакансий

Ближайшие события