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

Что такое превентивный интерфейс

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

type Auth interface {
  GetUser() (User, error)
}
type authImpl struct {
  // ...
}
func NewAuth() Auth {
  return &authImpl
}

Когда полезны вытесняющие интерфейсы

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

// auth.java
public class Auth {
  public boolean canAction() {
    // ...
  }
}
// logic.java
public class Logic {
  public void takeAction(Auth a) {
    // ...
  }
}

Теперь предположим, что вы хотите изменить Logic :: takeAction так, чтобы он принимал любой объект, если у него есть функция canAction (). К сожалению, нет. Auth не реализует интерфейс с canAction () внутри него. Теперь вам нужно изменить Auth, чтобы предоставить ему интерфейс, который вы затем можете принять в takeAction, или обернуть Auth в класс, который не делает ничего, кроме реализации вашего метода. Даже если в logic.java определен интерфейс Auth для приема в takeAction (), может быть очень сложно заставить Auth реализовать этот интерфейс. У вас может не быть доступа для изменения Auth, или Auth может находиться в сторонней библиотеке, которую сложно разветвить. Возможно, автор Auth не согласен с вашим интерфейсом. Возможно, вы поделитесь Auth с коллегами по кодовой базе, и теперь вам нужен консенсус перед его изменением. Вот каким вы хотели бы видеть код Java.

// auth.java
public interface Auth {
  public boolean canAction()
}
// authimpl.java
class AuthImpl implements Auth {
}
// logic.java
public class Logic {
  public void takeAction(Auth a) {
    // ...
  }
}

Если бы автор Auth изначально закодировал и вернул интерфейс, у вас никогда бы не возникла проблема при попытке расширить takeAction. Естественно, он работает с любым интерфейсом Auth. В языках с явными интерфейсами ваше будущее «я» будет благодарить свое прошлое за использование вытесняющих интерфейсов.

Почему это не проблема в Go

Давайте настроим ту же ситуацию в Go.

// auth.go
type Auth struct {
 // ...
}
// logic.go
func TakeAction(a *Auth) {
  // ...
}

Если логика хочет сделать TakeAction универсальным, владелец логики может предпринять это действие в одностороннем порядке, не мешая другим.

// logic.go
type LogicAuth interface {
  CanAction() bool
}
func TakeAction(a LogicAuth) {
  // ...
}

Обратите внимание, что auth.go не нужно менять. Это ключ к тому, что делает ненужными вытесняющие интерфейсы.

Непреднамеренные побочные эффекты вытесняющих интерфейсов в Go

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

Лучшее использование интерфейсов в Go

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

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