Comments 132
Если придерживаться такой концепции, то в будущем переписать много кода, чтобы что-нибудь протестировать. Ладно если свой проект. Но почему бы не сделать готовые интерфейсы для публичного пакета, чтобы это не делали люди сами?
В основном, я согласен, что интерфейсы часто пихают везде, где нужно и где не нужно
Но почему бы не сделать готовые интерфейсы для публичного пакета
Потому что это не джава.
Вот вы используете библиотеку X и не планируете никогда переходить на другие имплементации. Зачем вам интерфейс нужен? Он просто бесполезным грузом будет и мешает инлайнить короткие функции заодно.
Или, допустим, библиотеки A и B реализуют одну концепцию, но совсем по-разному. У них разные функции, с разными типами и разными аргументами. Пусть обе предоставляют интерфейс, но зачем он? Библиотека A будет описывать только свои методы, библиотека B только свои. Чтобы можно было их поменять, придётся все равно писать обёртку над одной или другой имплементацией.
Интерфейс в Go - это, как в конце отмечено, инструмент вызывающего кода, а не имплементаций. И хороший интерфейс реализует минимальный набор функций, который необходим конкретно вызывающему коду.
в определенных случаях, вполне имеет место быть и интерфейс по месту реализации, чтоб далеко не ходить за примером context.Context
Я не понимаю, почему все в пример приводят Java. Пожалуйста, на Java тоже прекратите писать интерфейсы где не надо.
Идея благая по намерению, но на практике готовый интерфейс очень редко совпадает с тем, что нужно тестам пользователя, лучше дать простой, прозрачный, конкретный тип.
Хотя опять же, только ситхи все возводят в абсолют, если у нас пара тривиальных методов и больше не предвидится, вай нот
Интерфейсы и предназначены чтобы вот такие умники не передавали структуры куда попало и потом удивлялись а че это там другой умник начал вызывать все методы подряд и расхерачил все данные внутри. Нет уж, вот вам через интерфейс только один безопасный метод и все
А зачем экспортировать то, что может что-то поломать? Никто не мешает умникам не использовать интерфейс или добавить в него запрещённые методы и продолжить расхерачивать.
Пока методы доступны снаружи, интерфейсы от такого не спасут. Они нужны не для этого. Для того, чтобы не расхерачить - просто нельзя давать принципиальную возможность расхерачить.
абсолютно верно, инкапсуляция достигается не интерфейсами
Никто не мешает умникам не использовать интерфейс или добавить в него запрещённые методы и продолжить расхерачивать.
Это если у умника есть доступ к собственно классу. А если он скажем от фабрики или DI получает сразу интерфейс, то он ничего этого сделать не сможет.
Для того, чтобы не расхерачить - просто нельзя давать принципиальную возможность расхерачить.
Вы можете внутри своей библиотеки иметь эти возможности для своих нужд. Но это не значит что их надо давать всем и снаружи.
П.С. Хотя естественно всё это спокойно решается и без интерфейсов. И интерфейсы скорее интересны для других вещей.
второй плюс - быстро понять за че этот SuperImportantDataService отвечает в системе.
в мешанине приватных методов, которые делают что угодно, искать 2 публичных метода, которые и есть часть интерфейса - такое себе удовольствие
…скажите это дизайнерам интерфейса Peugeot 🤣
очепятка в примере memStorage и mockStorage?
Вы показали примеры где интерфейс используется для сокрытия реальной зависимости и как абстрактное возвращаемое значение, но самое интересное обошли стороной - интерфейс как параметр функции. Хотелось бы услышать Ваше мнение по этому поводу.
Ну это как раз тот случай, когда интерфейс работает максимально по гошному)
Интерфейс у которого одна реализация не нужен. И никогда не был нужен.
Интересное мнение, но подумайте, в чём разница, отдаёте ли вы потребителю полную реализацию, либо имплементацию интерфейса с методами, необходимыми потребителю. Не думаю, что есть кейсы, когда вы в условном хэндлере UserByID будете вызывать метод (s *Storage) DeleteUser. Это абсурд. И от абсурда в частности спасают интерфейсы. Вас спросят "сколько стоят бананы?", вам для ответа на этот вопрос не надо знать сколько стоят яблоки:)
Приватные методы существуют. Не надо делать паблик все подряд и все хорошо будет.
Приватные здесь не помогут. Потому что они доступны только внутри самого класса.
Вариантом было бы использовать что-то вроде internal из C#. Если язык такое предлагает.
package private это называется. В Го такого модификатора разве нет?
а чего боимся-то?
одного файлика:
явно описывающего контракт
позволяющего бесшовно при необходимости заменить реализацию во всем коде просто заменив конкретный класс в DI
позволяющего заменить моком в тестах без привязки к конкретной реализации (привет любителям final)
все это не надо?
(хз за го-специфичные нюансы, я про ооп)
Контракт и так объявляется реализацией, очевидным образом. Все остальное, как правило, относиться к зоне ответственности того, кто ей пользуется. Go way не создавать ненужные абстракции. Интерфейс ради интерфейса не добавляет никакой ценности
нифига он не описывается реализацией: открываешь сервис, там 48 приватных и 2 публичных метода, каждый по 100+ строк, только один из которых вызывается извне и является контрактом.
ковыряться можно, но зачем, если можно не ковыряться?
Интерфейс ради интерфейса не добавляет никакой ценности
ну так интерфейс не ради интерфейса, а ради приемуществ, которые вы с ним получаете.
выше описал 3 явных плюса наличия интерфейса. цена - один раз его написать и обновлять сигнатуру (при необходимости).
я не понимаю людей, которые считают хорошей сделкой терять эти плюсы, сэкономив время на введении интерфейса (20сек?30?).
что вы более полезного за 30 сэкономленных секунд сделаете?
Есть документация плюс сигнатуру публичных методов и так видно в структуре их не надо искать. Если реализация написана хорошо, в ней легко ориентироваться, если там бардак, то интерфейс не поможет.
Лучший способ помочь читающему, это хорошо оформить свой код, на это надо тратить время, тк код пишется один раз а читается сотни и тысячи.
Это не вопрос экономии времени, философия языка в том, что интерфейсы, в общем случае (те, когда нет явных причин сделать по другому), принадлежат потребителю
Есть документация плюс сигнатуру публичных методов и так видно в структуре их не надо искать
в классе на 2 метода видно, в классе на 50 (40+ из которых - вспомогательные для тех самых 2, которые должны быть в интерфейсе) - не видно.
Это не вопрос экономии времени, философия языка в том, что интерфейсы, в общем случае (те, когда нет явных причин сделать по другому), принадлежат потребителю
вообще не понимаю этот пассаж.
вводя интерфейс, вы знаете какие плюсы вы получите. если вы осознанно не вводите интерфейс, отказываясь от плюсов, и не пытаетесь этим сэкономить время - то чем вы руководствуетесь?
что перевешивает плюсы?
Да. Лишний файлик, лишний код, повышение количества кликов и кода которые надо посмотреть чтобы добраться до логики, повышение нагрузки на человека читающего код.
Разумный минимализм это хорошо. Не надо делать лишнее.
лишний код, повышение количества кликов и кода которые надо посмотреть чтобы добраться до логики
любопытно, что всё с точностью до наоборот: чтобы понять контракт класса надо заковыриваться в его тело, искать там public методы которые используются извне.
это не разумный минимализм, а неразумный, и не минимализм
вы создаёте сложности читателю, ничего не предлагая взамен (отсутствие файлика, ну вы серьёзно?)
точка зрения "лишний код" здесь абсолютно абсурдна, хоть и понятна
любопытно, что
Оба аргумента применимы только к случаю, когда без IDE пишешь.
Так-то и клик давно умеет игнорировать интерфейс, сразу переходя в единственную реализацию, и список публичных методов показывается в специальной панельке, без необходимости их искать. Вроде бы даже есть специальный вариант, который просто схлопывает приватные методы в тексте, если хочется прямо на него смотреть.
а можно было просто посмотреть на интерфейс, и не полагаться на тулинг, но программисты экономят на файликах :D
сюр. я правда думал что мб у сторонников "не вводить интерфейсы" за спиной есть аргументы получше, чем "не создавать лишний файлик", но, к моему изумлению, это единственное что у них есть.
цена поиска контракта в классе с большим количеством методов игнорируется
цена удобства тестирования игнорируется
цена удобства замены реализации (никогда никогда не случится, да-да) игнорируется
это просто смешно
интерфейс
Зачем на него смотреть, именно в виде файла? Шмяк хоткей - и тебе методы показывают. Ну или в документацию отправил (она же есть, правда?) Чего именно в интерфейсе, без реализации, интересного?
А сам файл - в общем, скорее дань традиции языка. Может быть или может не быть. Все равно его руками писать необязательно.
Чего именно в интерфейсе, без реализации, интересного?
я буквально выше вам, и еще господину по 3 пункта выписал что интересного вы получаете с интерфейсом (вернее, что вытеряете не вводя интерфейс)
я буквально выше вам 3 пункта выписал что там интересного
Это объяснение, зачем нужен интерфейс как таковой. Может быть полезен, да.
А вот в виде отдельного файла - в общем, показания рознятся. Можно и компилятор на это дело натравить - пускай он его извлечением занимается.
Можно и компилятор на это дело натравить - пускай он его извлечением занимается
уже и компилятор натравили, и ide...
а можно просто ввести чертов интерфейс за десяток секунд.
вы явно не сторонник поиска легких путей 😁
а можно просто ввести чертов интерфейс за десяток секунд.
А можно не вводить, а просто написать 'public' в имплементации, а файлик с интефейсом сам сделается и поправится.
вы явно не сторонник поиска легких путей
Мой тезис в том, что если инструменты есть (а они есть) - то аргументы что той стороны, что той - слабоваты. Сейчас оно традиция практически в чистом виде.
А можно не вводить, а просто написать 'public' в имплементации
попросил гпт сгенерить для демонстрации.
https://gist.github.com/karrakoliko/662c6c509771031acb8a901d4c5149b9
удобно?
сколько времени вам нужно чтобы понять контракт сервиса?
а вот его интерфейс, глянув на который вам понадобится 0 секунд: https://gist.github.com/karrakoliko/5b503876e6997396f4d0e0f0661eeb34
сколько времени вам нужно чтобы понять контракт сервиса?
Ctrl-F 'public'
public function processData(array $data): array
public function summarizeData(array $data): string
Если бы код был в IDE - вообще специально искать бы не пришлось.
надо было ещё пару нерелевантных методов сделать public, и наплодить переменных со словом public, чтобы имитировать реальную ситуацию, в которой вы в этом классе застрянете минут на 20 отвечая на вопрос "какой контракт у этого сервиса".
ну, ваша логика мне понятна.
удачи вам, и комфортной работы :)
надо было ещё пару нерелевантных методов сделать public, и наплодить переменных со словом public, чтобы имитировать реальную ситуацию
А это пример, где отдельный интерфейс скорее вреден. Потому что провоцирует вот такое, когда думаешь 'а public в имплементации - это контракт или нет? В смысле - использовать можно или как?'
Угу. И до кучи реализовать одним классом два интерфейса ;)
Ну раз уж у нас интерфейс забота потребителя, вот мы одним махом двух закрыли.
Давненько не читал код на пхп, но точно помню что как минимум публичные методы пишутся в начале файла, приватные и протектед под ними. Даже если написано в таком стиле, вы же не в блокноте работаете, IDE и так показывает структуру
“Visibility MUST be declared on all properties and methods.
PSR-12 – Extended Coding Style Guide
к сожалению, нет указания порядка сортировки методов в зав-ти от области видимости, по крайней мере в общепринятом стандарте.
IDE и так показывает структуру
в gitlab мне кто структуру показывать будет?
вы придумываете какие-то альтернативные способы решения проблемы, которой не должно существовать, параллельно отстаивая право на создание этой проблемы.
это странно.
я бы понял если бы создание интерфейса было бы какой то невероятно ресурсоемкой, или хоть сколько нибудь сложной задачей, но нет же, реально секунды, и куча доп.плюшек за эти секунды.
Так буквально объявляете интерфейс с нужными методами по месту использования и ничего из описанных преимуществ не теряете. Зато связность кода ниже. Зависимость будет не от широкого Service, а от узкого Updater, Creator и т.д.
И реализацию в случае чего будет проще подменить, и декоратор написать, когда это небольшой интерфейс, а не портянка.
Если человек полез в код, то что-то там не так. Пока все хорошо человек вводит имя объекта жмет точку и выбирает метод.
Интерфейс мешает. Ты попадаешь не в код, а куда-то. Откуда надо пробираться до настоящего кода.
Это именно минимализм. Интерфейс не несет пользы и равен паблик методам. И зачем?
я раз 5 уже отвечал зачем в рамках этой и соседней веток, не буду повторяться, простите.
Если человек полез в код, то что-то там не так.
мало того что это не так, это еще и отношения к делу не имеет
Человек вводит имя объекта жмет точку и выбирает метод.
Интерфейс мешает. Ты попадаешь не в код, а куда-то.
Да нет, ты кликаешь на имя метода, на зеленую кнопочку (в jetbrains'ах так) чтобы провалиться в единственного наследника и все.
Работает и наоборот: проваливаешься из наследника в интерфейс.
Если наследников больше одного - ты выбираешь в какого именно провалиться, что тоже чертовски удобно.
Если реализаций больше 1, и интерфейс отсутствует - разработчика на переобучение, на ревью завернуть.
Я именно про случай одной реализации.
И зачем эти лишние клики и лишние файлики? Ценности в них нет. Язык для людей и исходники для людей. Упрощать надо.
в классе 15 методов, из них 3 публичных, 12 приватных, вспомогательных. нужно понять что это за класс и что он в нашей системе делает. а перед вами 15 методов.
упростили? удобно? хорошо, копайтесь в упрощенном.
вторая реализация (которой вчера "никогда не будет", а сегодня "все таки надо") - удобно и просто. всего лишь нужно ввести интерфейс, и обновить все связанные тесты и зависимости (чего можно было не делать, если бы ввели сразу)
новая зависимость в конструкторе? добро пожаловать в переписывание всех тестов что ее использует.
я потрачу 15 сек на выделение интерфейса сразу, буду мокать интерфейс в тестах, и по 2 клика сделаю каждый раз когда буду читать, ничего страшного.
деды из классических книжек учили на интерфейсах проектировать, у них были резоны, они рассказали и показали как писать для людей.
а вы копайтесь в своём "упрощенном", "для людей".
я потрачу 15 сек на выделение интерфейса сразу, буду мокать интерфейс в тестах
Для мока технически не надо интерфейса. Можно тестовый класс называть так же как оригинал и тестовой среде выполнения говорить 'Во время тестов используй то определение имени а не это'.
Что, собственно, и делается (но в другом виде), когда подстановка интерфейса происходит.
Но tooling пошел другим путем.
Можно тестовый класс называть так же как оригинал и тестовой среде выполнения говорить 'Во время тестов используй то определение имени а не это'.
Это работает далеко не во всех ЯП.
Кроме того вот это вот 'Во время тестов используй то определение имени а не это' тоже лишние телодвжения. Если такое начинается, то тогда уже проще действительно интерфейс сделать.
П.С. Это если у вас нет фреймворка, который позволяет классы тоже мокать. Что сейчас уже много где встречается.
Это работает далеко не во всех ЯП.
Механизм поиска библитек/имен в любом языке есть. Есть список каталогов и прочих мест, который просматривается для поиска библиотек.
В этом случае можно tooling делать так, чтобы первое объявление находило тестовую либу somelib с SomeName раньше, чем основную.
Но да, если можно писать (условно):
'from somelib use SomeName'
и оно в тестовой среде превращалось (явно или неявно) в
'from somelib.mock use SomeName'
то это заметно удобнее.
Механизм поиска библитек/имен в любом языке есть
Но далеко не везде вы можете просто взять и заменить один класс на другой. Даже если они одинаково называются. Компилятор не даст.
то это заметно удобнее.
Там обычно просто берёте класс и мокаете все его публичные функции и поля. Точно так же как с интерфейсами.
Компилятор не даст.
Это зависит от понимания 'не даст'. Потому что в пределе - выкидывается исходный текст оригинала, вставляется текст мока и компилируется.
"Не даст" значит выдаст ошибку при компиляции. Потому что на одинаковые имена классов ему наплевать.
То есть если такое делать, то это уже надо лезть под капот, использовать рефлексии или там пытаться менять что-то динамически во время исполнения. Что само по себе не особо хорошая идея и уж тем более для тестов. Проще потратить минуту на интерфейс :)
Это если у вас нет фреймворка, который позволяет классы тоже мокать
пока эти же умники не заставят пометить единственную реализацию final, чтобы она была ненаследуемая и немокаемая
Отлично все. Расположите методы нормально, это в любом случае надо, и удобно будет. Паблик наверх там и все такое.
Вторая реализация которой нет вероятнее всего и не появится. Не надо оверинжинирингом заниматься. Если появится тогда и сделаете.
Дело не только в кликах. Это дополнительная нагрузка на человека читающего код. А ее надо экономить.
Это дополнительная нагрузка на человека читающего код. А ее надо экономить.
это сюр:
выделение публичного интерфейса класса за неск секунд, вообще без участия мозга, почти на мышечных рефлексах - оверинжениринг и когнитивная нагрузка на читателя
все негативные последствия невыделенного интерфейса, которые я прям буквально перечисляю - полный порядок, "Отлично все.". просто надо бесконечный список инструкций как себя вести во всех ситуациях: от расставления методов в православном порядке до особенных инструкций по чтению кода + ограничение по количеству реализаций. выше даже компилятор предлагали костылить 😁
мне кажется если я вам буду на красный треугольник показывать вы мне ответите что не видите причин беспокоиться из за черного квадрата 🙈
я отказываюсь в этом участвовать, хорошего вам дня :)
в классе 15 методов, из них 3 публичных, 12 приватных
Вообще, это признак дурно пахнущего, не тестируемого кода. Суперкласс, который делает все и сразу, при малейшем изменении в котором придется править все тесты из-за связности.
Хороший код будет разбит на отдельные классы, функции, каждый со своей ответственностью. Если у вас появляется необходимость в приватном методе, стоит задуматься, а не стоит ли это вынести в отдельный класс, где это отдельно потом протестировать?
Нет, я не говорю, что приватных функций быть не должно. Но очень часто я встречаю именно такую ситуацию, что класс с кучей приватных методов потом невозможно поддерживать, если он покрыт тестами.
я с вами на 100, этот код - говно, но это то с чем приходится работать
если бы автор вынес хотя бы интерыейс - мне было бы значительно легче
В го нет классов, и все 15 методов можно размесить в разных файлах с понятными названиями в пакете, то есть пакет как класс. А кто использует 1 из 15 методов, сам у себя определяет что ему надо этот метод через свой локальный интерфейсc. Ему не надо знать о других методах - упрощается тестирование и контекст не теряется.
Давайте уточню, говоря класс, я имею ввиду структуру с методами. Просто вопрос привычки.
Как правило, приватные методы делают чтобы вынести часть логики из публичного, где они потом вызываются. Проблема такого подхода в том, что ты не можешь протестировать логику публичного метода в отрыве от логики приватного. Тесты становятся монструозными, связными. Их потом не хочется даже читать, не говоря уже о том, чтобы их править. Из-за этого такой класс превращается в сифу, которого никто не хочет касаться, пока не прижмет.
Гораздо лучше, если эта логика будет в отдельном классе, где будет протестирована отдельными тестами, а в тестах того публичного метода просто использовать мок.
Лишний файлик, лишний код
Лишний интерфейс - это тоже лишний код
Да, вот ради контракта вполне стоит заводить. Контракт ясен читателям, ясен и для AI - которое может сгенерировать реализацию.. Ну и в тестах, само собой, подменить легко....
Экспортируемые методы реализации это и есть контракт. Интерфейс тут ничего не улучшит.
Можно поинтересоваться, как у вас ведётся разработка? Я вот предпочитаю сначала описать именно что контракт без реализации - и интерфейс тут самое оно. Т.е. пишешь, никому не мешаешь, компилятор не ругается - и всё такое.
Все описал, продумал - потом уже реализация. Опять же - во многих случаях по интерфейсу код с тестами успешно генерируются при помощи AI.
Экспортируемые методы реализации это и есть контракт. Интерфейс тут ничего не улучшит.
А для чего тогда вообще интерфейсы нужны?
выше описал 3 явных плюса наличия интерфейса. цена - один раз его написать и обновлять сигнатуру (при необходимости)
Цена - написать, постоянно при изменении сигнатуры обновлять ее в двух местах, при навигации по коду постоянно попадать на интерфейс и постоянно нажимать сочетание клавиш еще раз чтобы перейти к реализации, постоянно тратить на это внимание и усилия. Особенно когда там несколько уровней вызовов, и каждый блин через интерфейс. Потом еще обратно двигаться, когда понял что тут нет деталей реализации которые нужны.
сэкономив время на введении интерфейса (20сек?30?)
От 4 до 10 секунд каждый раз при навигации по коду (это может быть десятки и сотни раз при работе по одной задаче), несколько минут при изменении сигнатуры методов, и дополнительные усилия и внимание, потому что вместо 2 мест вызывающий и вызываемый код, между которыми можно переключаться одним сочетанием клавиш (обычно Ctrl+Tab), появляется 3 места, с которыми идет работа в произвольном порядке.
чтобы понять контракт класса надо заковыриваться в его тело, искать там public методы которые используются извне.
Не надо. Если вы пишете код, надо поставить в IDE точку, или стрелочку, или нажать что-то вроде Ctrl+Space. Если не пишете, то и список публичных методов вам не нужен, читайте то что есть.
вы боитесь увидеть "лишний" интерфейс, но не боитесь глазами каждый раз заново выковыривать публичный контракт из класса
цена поиска контракта в классе с большим количеством методов игнорируется
Потому что так как вы никто не делает. Нет никакого "каждого раза". Пишешь стрелочку, получаешь список методов, которые можно вызывать.
2 публичных метода, каждый по 100+ строк, только один из которых вызывается извне и является контрактом.
Если там 2 публичных метода, значит оба они являются контрактом. Если это ошибка, то в интерфейсе можно тоже ее допустить.
если видишь public метод вне интерфейса на ревью - тут же заворачиваешь.
В этом случае интерфейс решает вам проблему соответствия между классом и интерфейсом, которая без интерфейса просто не существует. Проблему когда в интерфейсе объявлено 2 метода, из которых используется только 1, он не решает. Вы просто перенесли ее из одного места в другое.
чтобы понять контракт класса надо заковыриваться в его тело, искать там public методы которые используются извне
В разработке это не надо. На ревью это не поможет, потому что неиспользуемый метод может быть объявлен в интерфейсе. Ревью делается один раз, а навигация по коду происходит постоянно.
а можно было просто посмотреть на интерфейс, и не полагаться на тулинг
Я пишу с тулингом, и отсутствие лишнего интерфейса упрощает мне работу. Мне пофигу на программистов, которые пишут код в блокноте, усложнять себе работу ради них я не хочу. Если вам это нужно для ревью, можно настроить инструмент анализа кода и добавить его в пайплайн, это также решит проблему когда лишний метод объявлен в интерфейсе.
позволяющего заменить моком в тестах без привязки к конкретной реализации (привет любителям final)
В вашем подходе нужно попросить любителя final, чтобы он указал ваш интерфейс в своей реализации. Точно так же можно его попросить просто не писать final.
Я рекомендую вместо final сделать правило для линтера, чтобы наследование было только от абстрактных классов. Тогда не будет проблем с моками в тестах.
цена удобства тестирования игнорируется
Во многих языках можно сделать моки на класс без интерфейса.
позволяющего бесшовно при необходимости заменить реализацию во всем коде просто заменив конкретный класс в DI
Не вижу в этом ничего положительного. Допустим, вы заменили один класс на другой с таким же интерфейсом, но который ничего не делает. Будет у вас вызывающий код работать как раньше? Нет. Надо найти все места и оценить, как повлияет замена, протестировать по возможности. Можно оценить количество затронутых мест, когда у вас на ревью изменена одна строка? Нет.
цена удобства замены реализации игнорируется
Поискать в IDE все места использования класса и заменить на другой занимает пару минут, никакой проблемы в этом нет. Также это даст понимание эффектов, которые окажет замена, что нужно учесть при выкладке, что протестировать. Замена сразу везде требуется редко, в большинстве случаев надо в части мест заменить, а в части оставить.
вообще всё, что вы привели в пример, высосано из пальца.
постоянно при изменении сигнатуры обновлять ее в двух местах,
главное в обморок не упасть в процессе, это ж целые ctrl+c,ctrl+v раз в полгода
Допустим, вы заменили один класс на другой с таким же интерфейсом, но который ничего не делает. Будет у вас вызывающий код работать как раньше? Нет.
+- так же, это всего лишь другая реализация того же процесса. для этого и пишут тесты, чтобы эти регрессии ловить автоматически, а не бегать глазами искать где что и как используется.
Поискать в IDE все места использования класса и заменить на другой занимает пару минут, никакой проблемы в этом нет.
полностью противоречите вами же сформулированной выше проблеме "Надо найти все места и оценить, как повлияет замена, протестировать по возможности". там где вы доказываете что отсутствие интерфейса хорошо- там это не проблема, а там где спорите со мной - ух, великая проблема, надо же все сайдэффекты оценить 😁
От 4 до 10 секунд каждый раз при навигации по коду (это может быть десятки и сотни раз при работе по одной задаче), несколько минут при изменении сигнатуры методов, и дополнительные усилия и внимание
какие доп усилия, какое доп внимание? какие десятки секунд на переход от интерфейса к единственной реализации? очнитесь, о чем вы 🙈
Вы задали вопрос другим людям, почему им не нравятся лишние интерфейсы. Я вам объяснил почему. Мне без разницы, если вы не считаете это проблемами. Я считаю это проблемами, у меня это значительно отвлекает внимание, поэтому мне не нравятся лишние интерфейсы.
Если вам не нужен ответ, непонятно зачем вы спрашивали тогда.
это ж целые ctrl+c,ctrl+v раз в полгода
Пока делается задача, для которой он появился, изменения происходят постоянно. А если у вас для половины классов есть интерфейсы, изменения в каком-то интерфейсе нужны почти для каждой задачи.
противоречит сформулированной выше проблеме "Надо найти все места и оценить, как повлияет замена, протестировать по возможности"
ух, великая проблема, надо же все сайдэффекты оценить
Вы неправильно поняли написанное. Это не описание проблемы, которую нужно избегать, а описание действия, которое подразумевается правильным, и которое избегать не нужно.
Мне кажется, вы не совсем понимаете, зачем нужны интерфейсы в любом языке программирования.
В частности, особенности структурной типизации интерфейсов в Golang часто вызывают затруднения у тех, кто привык к объектно-ориентированному программированию.
Надеюсь, вы знакомы с концепцией, когда интерфейс принимается на вход, а на выход отдаётся структура и почему так.
Это не так - вы наверняка в каждой второй функции возвращаете интерфейс ошибка
Но надо понимать что стандарная бибилотека может возращать интерфейс как исключение, но не прикладные.
Да и прикладные возвращают. Я вот пишу сейчас код для ClickHouse. Там интерфейс возвращается при создании клиента. Создателям языка задавали про это вопрос и они сказали, что нет такого правила возвращать структуру
Ну, если это верхнеуровневая библиотека, то как реализация фабрик (драйвер, например) еще годится отдавать интерфейс. Но если делают такое в прикладных сервисах, как, например, принято в Java/PHP, тогда будут проблемы, я думаю. И самое главное - получится со временем "комок грязи", при этом проект придет к такому состоянию незаметно. Особенно когда проект большой и много людей с ним работало. Изменение в одном месте может привести к изменениям не связанных с задачей местах, и нет гарантии, что данные изменения не поломают в других местах. Сильную сторону реализации в Go типизации (утиная) для разделения на пакетном уровне не используют и применяют методики из других ЯП с полным ООП. Когда на такой проект смотришь со стороны, то он выглядит чужеродно на Go и все жестко "прибито гвоздями" даже с интерфейсами друг к другу. А так вы правы, нет жесткого стандарта в Go, и каждый «художественный коллектив» делает, как договорятся в команде. Я вижу, что даже с опытом на ЯП типа java/php то, что есть в го, не сразу заходит, и я тоже сам не сразу в "тему" вошел).
Возвращаясь к примеру выше, такой подход привычен разработчикам, которые приходят из других языков, потому что там интерфейс это исходная точка проектирования.
Это какие же языки имеются в виду?
Я знаю ровно один, и это не совсем язык — COM. Там это справедливо на все сто, и обусловлено техническими причинами. Если вам нужно добиться бинарной совместимости между любыми ЯП без виртуальной машины, то ничего другого не остаётся, и публично кокласс может только имплементировать интерфейс.
Во всех остальных случаях это крайне дубовая идея, к сожалению, действительно, нередко встречающаяся. Лечить её авторов можно следующей метафорой. Есть Бенедикт Камбербетч и есть Василий Ливанов. В какие-то моменты они ведут себя одинаково — как Шерлок Холмс. Поэтому есть смысл прописать Шерлока Холмса во всех деталях. А есть Артур Конан Дойл, который всегда ведёт себя как Артур Конан Дойл. Описывать его как Артура Конан Дойла, который играет Артура Конан Дойла, нет никакого смысла.
Описывать его как Артура Конан Дойла, который играет Артура Конан Дойла, нет никакого смысла.
Это тот который Писатель, Доктор, Историк, Мужчина, Отец и тд и тп?
Что плохо с метафорами, это то, что всегда с её помощью можно повернуть разговор в такое русло, что автор метафоры обалдеет. Написание программы это моделирование. Когда вы смотрите кино, снятое по книге, вам обычно всё равно, каким отцом или доктором был автор. Это просто за пределами модели. Ну а если это артхаус из серии «Тарковский — гений!», где докторство писателя рифмуется с докторством Ватсона, то пожалуйста, вводите интерфейс IDoctor.
Ладно, поговорим без метафор. Я знавал одного программиста, который не писал просто класс Window, а всегда сначала описывал IWindow, а потом его реализовываал. То есть, буквально следовал принципу «интерфейс это исходная точка проектирования». Зачем? Спросите его. Я не знаю, зачем. По мне, это чистое вредительство. Создаётся впечатление, что оконность это роль (что само по себе упячный тезис, плохо поддающийся осмыслению), но по факту в его коде оказывалось, что это просто не так.
Ещё я работал в коллективе, где почти у каждого был свой ЯП и среда: VB, C++, MC++, Delphi, C#. Всё это увязывалось через COM. Там понятно, для чего так делать — по техническим причинам. Там же не передашь Window, передать можно только IWindow. Но у того программиста, про которого я говорю, была гомогенная среда (100% C#).
Поэтому, когда я прочитал, что «такой подход привычен разработчикам», мне стало любопытно. Помимо тех случаев, которые известны мне, где ещё такое практикуется? (За пределами COM'а).
Interface segregation principle? Нет, не слышал xD
А с ним все в порядке. Смысл ISP в классических ооп языках состоит в том, чтобы заставить автора разделять крупные интерфейсы на маленькие, чтобы потребитель не был вынужден реализовывать методы, которые ему не нужны. В го интерфейс вообще не обязан существовать до тех пор, пока его не потребуют. ISP проявляется, а на стороне потребителя который создаёт настолько маленький интерфейс, насколько ему нужно, так что все ок с соблюдением принципа
плюс если автор следует принципу единственной ответственности (SRP), то поверхность апи у реализации и так будет небольшой
А Dependency inversion? Без интерфейса получается зависимость от конкретной реализации. Ну и оно может не выстрелить, а может и выстрелить. Поэтому кажется проще сразу добавить интерфейс и зависеть от него. Также с другими принципами, можно не соблюдать ради скорости\простоты\чего-то еще, но потом разгребать с большими усилиями, чем если бы соблюдали изначально.
А Dependency inversion? Без интерфейса получается зависимость от конкретной реализации.
От имени. В случае необходимости бывшая конкретная имплементация делается интерфесом, а новая уже его реализует. Или сразу другая реализация вставляется под старым именем.
Но в рантайме замена уже не сработает, это да, надо будет перекомпилировать.
Это, кстати, одна из причин появления заметного числа классических правил. Они исходят из предположения, что перекомпиляция и пересборка - это такая штука, которую нужно бояться как огня.
А теперь давайте представим как это должно работать в случае если у нас всякие там плагины и/иои динамически подгружаемые библиотеки...
printf из условной динамической libc.2.4.5 и libc2.4.10 влинковываются в приложение именно как разные имплементации одного и того же имени. Какая стоит в системе (и printf называется) - ту и используем.
А printf_v_2.4.5 implements printf, printf_v_2.4.10 implements printf - об этом пользователю либы думать не надо. Он только про printf в общем случае знает. Да и в коде библиотеки этих двух имплементаций, вроде, нет. Есть одна printf, которая меняются только вместе со сменой версии кода.
Можно, конечно, сказать что та printf, что в stdlib.h -- это и есть интерфейс, но весь этот механизм Dependency Inversion почему-то не называют и в пример не приводят. Хотя стоило бы.
Но да, тут - интерфейсы полезны. А если такой динамической (пере)конфигурации, прямо в рантайме - нет, можно и без них.
на стороне потребителя который создаёт настолько маленький интерфейс
А если потребителей(пакетов) несколько то и интерфейсов(дублей) несколько? И для каждого потом генерировать мок? Или сделать ещё один пакет на стороне потребителя и описать все там?
Go как раз идеально стыкуется с этим принципом. Зато в условном шарпе почти в каждом проекте есть какой-нибудь IUserRepository, в котором перечислены 50 методов для чтения и обновления юзера в БД.
Мы со школьных лет привыкли мыслить в парадигме таких языков как delphi / java / c++
как же хорошо вы думаете про людей
Посыл не очень понятен, честно говоря.
Основная фишка в go это утиная типизация. В какой-нить Java вы должны явно указать, что класс реализует интерфейс. В Go так делать не надо. Плюс go-way это объявление интерфейсов в месте использования, а не в месте реализации.
Это значит, что из изначального примера вы можете прям в месте использования определить, что вашей функции нужен интерфейс OrderLoader и там будет только метод Load(), а другой функции нужен OrderSaver только с методом Save(), а в конечной реализации это не обязательно будет один объект (кто же заставляет все методы в один Storage пихать?)
В go есть ровно 2 ситуации, когда стоит обьявлять интерфейсы - для мока зависимостей в тестах и для поддержки нескольких реализаций.
На этом все, никаких больше причин создавать интерфейсы в го нет, кроме тяжелого наследия у разработчика из его опыта на других языках программирования.
Дык оно, имхо, для любого языка так, не? Либо для тестов, либо для лёгкой замены имплементации.
Так это как раз и причина почему их стоит и прилепить на вход и на выход
И по этому как раз я и не согласен с автором статьи
Пожалуй добавлю третью: чтобы разорвать циклические зависимости.
3: быстро соориентироваться в чужом коде, чтобы понять контракт класса и соотв его роль в системе
нет никакой гарантии что автор кода адекватен
- он может делать 50 методов в классе, 2 из которых - контракт, остальное - хэлперы. сиди и ковыряйся?
- извне вызываться будут не те публичные методы, что в интерфейсе
но вероятность всё же высока, а исключения - исключения, а не правило
Интерфейсы в го инструмент точечного применения, а не стартовая точка дизайна. Когда их начинают плодить заранее, код только тяжелее становится
Возможно, из-за того что я тестирую 100% своего кода, создание интерфейсов у меня же на подкорке. С текущим бэкграундом не могу с вами согласиться.
Можно написать декоратор и подменить реализацию без изменения потребителей, если зависимость уже от интерфейса. А в протестированном коде зависимость почти всегда такая.
Например Storage заменить на CachedStorage, который является декоратором над Storage.
Во многом ваши мысли перекликаются с моей статьёй о функциональном проектировании.
оверхед - слово конечно старшное, но в ентерпрайзе не сильно существенное. А вот что может быть существенно, так это легкость тестирования.
имхо есть 2 путя тут:
1. когда в типе акцент идет на сами хранящиеся в нем данные, тогда расширяющая его (тип) функциональность в интерфейсе не нуждается, т.к. эта функциональность скорее всего будет очень сильно переплетена с самими данными.
2. использование интерфейсов в сервисных (стейтлесс) типах (что в целом является большинством кода который мы пишем), где по возможности имхо лучше использовать интерфейсы по максимуму:
а. интерфейсы на стороне клиента что бы ограничить набор нужного для работы апи и упростить тестирование СЕБЕ.
б. интерфейсы на стороне реализации, что бы по возможности под них сразу положить рядом моки и упростить тестирование КОМУ-ТО.
Согласен со всем, кроме этого:
интерфейсы на стороне реализации, что бы по возможности под них сразу положить рядом моки и упростить тестирование КОМУ-ТО.
Один клиент использует гомок, второй мокери, третий еще что-нибудь. У вас одна версия гомока, у клиентов другая, они могут быть несовместимы (или совместимы формально). Из недавнего могу вспомнить, когда в go.uber.org/mock заменили метод IsGomock на поле isgomock struct{} в рамках одной мажорной версии.
А потом вы хотите перейти с гомока на мокери, но не можете, т.к. старые моки уже кто-то использует и удалять их без повышения мажорки нельзя. И пакет превращается в помойку из набора моков всех видов.
Моки должны генериться рядом с тестами, потребителем класса. Такие моки, которые нужны именно ему. Все остальное только создает проблем.
Мне кажется, что вопрос "что возвращать из функции?" - интерфейс или структуру, во-первых, особого отношения к необходимости определения интерфейсов не имеет, а во-вторых, в случае "утиных" интерфейсов не особо то и значим (во всяком случае, гораздо менее значим, чем в случае номинальных интерфейсов).
Гораздо важнее - что принимать на вход. Другими словами, на каком уровне описывать зависимости, на уровне интерфейсов или на уровне реализаций. Здесь, вроде как, ответ однозначен - [за исключением очень маленьких систем] зависимости нужно определять на уровне интерфейсов. Что, в общем, не противоречит изложенному в статье. Но вот к подходу "Интерфейсы принадлежат потребителю" есть один вопрос - а что если потребитель не один? Кому из них будет принадлежать интерфейс? Interface segregation - уже сделано, интерфейс всё равно нужен больше, чем одному потребителю, не режется он дальше.
По поводу "...тем больше интерфейс разрастается и целиком он никому не нужен...". Все же, думаю, понимают, что функциональные интерфейсы, которые доводят идею interface segregation до логического завершения - это не панацея и не решение всех проблем (фанатичные функциональщики здесь будут сильно возражать, но на Go они обычно не пишут).
когда потребителей несколько, интерфейс можно вынести в слой. Важно, что он всё равно формируется от требований использования, а не от реализации, просто потребителей становится несколько.
интерфейс можно вынести в слой
а можно и не выносить, да? и чтобы его вынести в отдельный слой, он же должен быть? а вы точно знаете заранее, сколько потребителей у вас будет? а через год?
он всё равно формируется от требований использования
в промышленной разработке ВСË формируется исходя от требований использования, каждая строчка.
а не от реализации
интерфейс никогда не проектируется "от реализации", в этом его смысл
пока потребитель один интерфейс живет приватно, на уровне пакета потребителя, не надо ничего преждевременно делать.
Тут ещё много вопросов можно задать, например о том, как, по вашему, автор потребителя №2 узнает о том, что в пакете потребителя №1 интерфейс уже есть (мы же не про утилитку на 100 строк говорим, мы же о чём-то побольше, где эти проблемы действительно есть), кто и когда решение о создании/выносе интерфейса будет принимать, и на какой стадии проекта, но это всё, по большому счёту, не важно.
Важную вещь выше написал @karrakoliko - вы все эти проблемы себе на ровном месте создаёте для чего? Ради соответствия принципу "Interfaces belong to the consumer", который даже не официальный и не поддерживается автором языка? Ради экономии нескольких строк кода?
Вот что вы получаете в замен? Ответа я не увидел ни в одном комментарии.
Я, пока еще не дочитал до конца, но пока не забыл:
При этом часто считают, что если вы вернёте из конструктора конкретный тип, а не интерфейс, то раскроете реализацию
Это моветон же. За исключением пары, весьма конкретных случаев, когда принято возвращать интерфейс (error, context, etc..) - Return structs Accept interfaces
Именно принято, писать интерфейсы, когда мы "поглащаем" что-то внешнее, возвращать интерфейс без надобности - фу-фу-фу. Можно его описать и экспортнуть, тем самым заложив контракт, но возвращать интерфейс надо, статически, почти никогда.
Все так, не знаю кто вам минус поставил
С этим трудно спорить. Интерфейс годится, когда ты что-то потребляешь. А возвращать его без реальной причины, только усложнять API. Тип и так достаточно инкапсулирован
accept interfaces, return concrete types
Лишние интерфейсы часто не добавляют гибкости, зато добавляют головную боль.
Авторы языка же прямым текстом говорили - интерфейс это про поведение, нужное потребителю, а не про скрытие реализации. Но в голове всё равно сидит джавовский паттерн "сначала интерфейс, потом мир вокруг". От него долго отучаешься
У меня такое чувство, что статьи об интерфейсах в Go выходят раз в полгода, и каждый раз бурность обсуждения показывает, что статье не удается быть ни убедительной, ни полной.
И дело тут не в авторах - они искренне стараются, а в экстремальной сложности темы. Не следует забывать, что именно эти правила осознанно не были добавлены на витрину Go Proverbs:
Interfaces belong to the consumer (отсутствует)
Accept interfaces, return concrete types (отсутствует)
И для второго правила, Роб Пайк, соавтор языка Go, лично добавил:
I'm not a great fan of that one. It's subtle and tricky to explain compactly. And although popular, it's not often easy to apply well... It's very hard to be clear about where it applies, and it often doesn't.
Одной этой фразы должно было быть достаточно, чтобы насторожиться - легко не будет. Но мы все так же наивны и недооцениваем. Забавно.
А как же чистая архитектура Роберта Мартина? В ней тоже теперь вместо интерфейса использовать конкретную реализацию?
Также раз был затронут такой аспект как overhead (который безусловно нельзя отрицать), возникает вопрос - каков он? Насколько он влияет? Если рассматривать именно с технической стороны, а не с чистоты кода
Прекратите создавать интерфейсы