Почему интерфейсы лучше размещать в месте их применения?
Что проверяют
Знаете ли вы, что в Go интерфейс реализуется неявно, и понимаете ли, что из этого следует для структуры пакетов. Это вопрос про направление зависимостей, а не про вкусовщину.
Как отвечать
В Go тип не объявляет, что реализует интерфейс: соответствие определяется по набору методов. Значит интерфейс может объявить тот, кто его вызывает, а пакету с реализацией об этом знать не нужно. Отсюда правило: интерфейс определяется на стороне потребителя, а конструктор возвращает конкретный тип.
package report
type store interface { // нужен только этому пакету
Get(ctx context.Context, id string) (Item, error)
}
func New(s store) *Service { return &Service{store: s} }Что это даёт:
- интерфейс получается маленьким: потребитель перечисляет только те методы, которые зовёт, а не копирует всё API хранилища;
- зависимость разворачивается:
reportне импортирует пакет базы, поэтому цикла импортов не возникает; - подмена в тестах бесплатна: рядом с тестом пишется структура на два метода, генератор моков не нужен;
- реализация свободна: добавить метод в конкретный тип можно, не трогая ни одного потребителя.
Уместно сослаться на формулировки из сообщества: принимать интерфейсы, возвращать структуры, и чем больше интерфейс, тем слабее абстракция.
Где спотыкаются
Заводят пакет с общими контрактами, куда складывают все интерфейсы сразу. Он импортируется отовсюду, любая правка касается всех, и смысл неявной реализации теряется.
Второе: объявляют интерфейс рядом с реализацией «на будущее», под единственный тип. Пока потребитель один, это лишний слой: он ничего не разворачивает и только мешает ходить по коду переходом к определению.
Третье: возвращают интерфейс из конструктора. Потребитель теряет остальные методы типа, а типизированный nil внутри интерфейса даёт известную ловушку: значение не равно nil, хотя внутри лежит пустой указатель.