В языке GO интерфейсы отличаются от других языков. Они немного лучше чем в других популярных языках с точки зрения дизайна. В этой статье я постараюсь объяснить почему.
Я расскажу о преимуществах, приведу примеры и рассмотрю некоторые вопросы, которые могут возникнуть при использовании интерфейсов.
В чем особенность интерфейсов в GO?
В большинстве языков вы описываете один интерфейс, и реализуете их в других местах явно, указывая, что вы реализуете именно их. Но в 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 кажется подходящим инструментом для развития больших проектов. И интерфейсы — это одна из тех фич, которая сделана удачно.
iamwizard
Эту, не уникальную для Go, особенность называют «Утиной типизацией»
anonymous Автор
Спасибо за информацию. Об этом я не знал. С этим впервые столкнулся в GO, поэтому взял именно его для рассмотрения.
Gorthauer87
А еще есть более умный термин структурная типизация.