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

Как мы избавились от 80% своего кода, повысив скорость разработки и уменьшив количество ошибок

Время на прочтение12 мин
Количество просмотров63K
Всего голосов 98: ↑90 и ↓8+82
Комментарии101

Комментарии 101

Service interface в большинстве случаев тоже не нужен.

Ну в принципе всё правильно. Абстракции ради абстракций и паттерны ради паттернов - это те вещи, которые сразу выдают неопытных разработчиков.

Но обычно с годами это проходит. После Х лет опыта люди снова начинают писать простой код. И, как правило, он работает намного быстрее и надёжнее нагромождений абстракных фабрик декораторов шаблонных обсерверов :)

Только обычно мало у кого, как у автора статьи, хватает храбрости удалять старые ненужные абстракции и паттерны, написанные неопытными разработчиками много лет назад. Так они и существуют засоряя код.

Иногда храбрость есть, а вот начальство говорит "мы не будем тратить на это время".

тут следует найти соратника ))

бывшая жена пришла в компанию с 3мя имеющимися разрабами, потом еще 2их приняли, "специалисты", выходцы из сопоставимой с Китаем, по популяции, страны, постоянно конфликтовали с бывшей на уровне фундаметальных знаний... что поддерживалось начальником отдела, киви. Но за год проект очень слабо сдвинулся, наняли еще двух контрактников, киви... Пара совещаний, с участием контрактников, неожиданно, "по семейным обстоятельствам", уволились две специалистки, через месяц еще двое. Сейчас в отделе осталась моя бывшая и один контрактник, и результативность отдела выросла в сотни раз! Ну короче стали появляться результаты. А... к чему это я? Весь код, который наплодили "специалисты", удалили на... и за месяц написали новый. А еще, в течении года, система несколько раз ложилась, причем боевая, из-за того, что "специалисты" инжектировали свой "гениальный код" не проверив, даже не тестовой системе, т.е. оно написало пару процедур, проверило - как бы работает, переосмыслило, переписало, дописало нового еще десяток, ну и... т.к. две недели назад проверка была, кидает в рабочую систему, и .... ах?! да... не работает даже та часть, которая "проверялась". И да, сейчас у них обсуждается таки вопрос удаления и старого, 5-10и летней давности кода, т.к. он не оптимален, но что будет, вопрос... т.к. у бывшей предложение от головной компании, т.е. ей предлагают работу сильно выше, чем её начальница, которая поддерживала все иннициативы "специалистов", и таки да... сейчас она с пеной у рта продвигает любые иннициативы бывшей, чуть ли не кофе ей носит.

иногда приходится сделать небольшой саботаж с целью переубедить начальство)

небольшой саботаж с целью переубедить начальство

Только чтоб большим не стал


Короткометражное кино в тему

английский оригинал — https://www.youtube.com/watch?v=MEOZkf4imaM
русский дубляж — https://www.youtube.com/watch?v=By4NE1LgigQ

Неопытный разработчик всегда имеет полярную точку зрения на абстракции - он или их избегает, или фанатично применяет.

Абстракции это простейший инструмент для описания неопределенности. Если человек пишет код для одной и той же предметной области несколько лет, и видит повторяемость-порядок, то немудрено, что это приводит к тому что абстракции становятся избыточны и появляется "простой код". Для разработчика главное, что бы его не деформировало это, иначе он может "сломаться" и посчитает, что "простой код" можно проецировать на любую предметную область. Вывод: Все очень относительно, я бы на вашем месте не был так категоричен на счет абстракций.

для неопытных это оооочень стандартно, услышать/вычитать о какой-то новой технологии, как пример, и начать с ней носиться, даже не понимая ее реальной сущности

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

Я встречался с обоими полярными вариантами у неопытных разработчиков - и лапшекод, заточенный под конкретную задачу, и решение сферических задач в вакууме при помощи нагромождения лишних абстракций. Но, как показывает практика, первое чуть-чуть лучше. Код "в лоб" хоть как-то поддаётся рефакторингу и отладке - да просто потому, что он более-менее ясен и его можно в худшем случае просто выбросить и заменить "нормальным". Код же "джуна-заумника", как правило, намного ужаснее - он размазан по всему проекту мелкими кусочками и пока найдёшь концы с концами и поймёшь, что хотел сказать этим (обычно уже уволившийся) автор - можно получить пару седых волос.

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

ЗЫ. Хотел про интерфейсы ещё отдельно написать, но кто-то карму зело слил, так что наверно только завтра смогу...

А при чем тут менеджер? Менеджеры не должны лазить в код никогда. И они не должны что-то пускать или не пускать в прод. Их дело пользовательские фичи, а что там в коде их не касается.

Есть времена когда скорость важнее. Так бывает. Фигачим в прод, на изоленте держится и ок. Надо не забывать писать беклог и каждые две недели напоминать всем до кого можно дотянуться о необходимости рефакторинга. Потому и потому. С доводами в общем. Бизнес обычно не собирается умирать через год и адекватные рефакторинги одобряет через какое-то время.

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

В жизни обычно некая суперпозиция этих двух варинатов. Блюдем баланс и хорошо. И фичи делаются за разумное время, и код поддерживается приемлимым.

Хуже только то, что иногда в проект приходят новые молодые разработчики, решают выбросить старые абстракции и запилить новые в том же количестве, только заменяя Java на Kotlin, например. А старых разработчиков с опытом, которые могут сказать "вот эту пачку интерфейсов мы ни разу не использовали" просто увольняют.

а еще, из живого примера, старые могут уйти сами. Недавно ушел разраб, с 30и летним опытом, т.е. собственно он один знает что зачем в системе, по причине "я устал объяснять, что "это не будет работать, и почему"". Свежие светлые идеи очень часто валят всю систему или большие её части, в случае, когда "специалист", не в курсе, что в компании существуют другие подразделения, работающие с системой по другому

Ага, у нас так пишут - "простые функции" вместо классов/методов/декомпозиции, которые разростаются до 200 строк с 50-70 комментариями, потому что ноль абстракций, а рефакторить никто уже не будет. Потом две недели на обычную багу, где два дня только собираются духом в этот код лезть. :)

В результате разработчики приходят и уходят (американские, в FAANG), а так как код ни туда, ни сюда не расширить, ибо все в куче, то обрастает такими костылями, от которых кровь стынет.

Хорошая статья.
Регулярно обсуждаем с коллегами проблемы чрезмерной "архитектуры" в отрыве от ее пользы.

Щас насоветуете тут избавляться от абстракций - как же тогда собесы проходить?

Одно дело спортивное программирование собеседование, другое — реальный код
спойлер
На собеседовании:
— Как поменять местами значения переменных не используя третью?
— A=A^B; B=A^B; A=A^B;
На работе:
— Что это ты за код написал?
— Меняю значения двух переменных не используя третью.
— Ты уволен.

Кстати задание на собесе само по себе странное: присвоение в большинстве случаев - это создание указателей, а не копирующий конструктор.

Красавцы. Все делают микросервисы, чтобы легче управлять зависимостями и тестировать отдельные модули. А вы собрали монолит, который в себе будет содержать дополнительную сложность.

Истина где-то посередине. С одной стороны 5 слоев абстракций конечно во вред. И лишние интерфейсы не нужны. С другой стороны они позволяют избежать спагетти кода, и внятно и коротко описать и протестировать логику работы, прочитать и быстро понять код.

Но при проектировании все не учтешь. Потому сначала пишется решение которое работает, а потом причесывается и разбивается на блоки так, чтобы было легко дальше поддерживать.

Если немного подумать. то зависимости между микросервисами это тот же спаггети код монолита, которые точно также нуждаются в отладке и могут резко вырасти в сложности и превратитьтся в зиккурат.

Можно и на микросервисах кашу сделать. Не панацея. Это с опытом приходит как рефакторить и где бритвой Оккама проводить. Тупое следование правилам, конечно не поможет. "Всюду микросервисы!" - "Всюду фабрики" - "Склеить все воедино!". Крайности не помогут.

Потому сначала пишется решение которое работает, а потом причесывается и разбивается на блоки

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

Хорошо если это большой проект/компания/стартап, где можно успеть что-то сделать, но в маленьких "проектах" это зачастую приговор.

А может это не так уж плохо для бизнеса, если бизнес не слишком зависит от эффективности кода? (Что не всегда так)

Все делают микросервисы, чтобы легче управлять зависимостями и тестировать отдельные модули.
Не нужно разбрасывать приложения по сети, чтобы получить обычную модульность.

1) Улучшенный дизайн выглядит подозрительно с точки зрения создания юнит-тестов. У вас есть юнит-тесты? Не интеграционные, а именно юнит-тесты. Написание тестируемого кода накладывает "ограничения" на дизайн. Отсутствие репозитория сразу бросается в глаза, как это тестировать.

2) Декоратор, который приведен в статье мог использоваться для "быстрого фикса", с последующим рефакторингом, то есть возвращению обратно к простому дизайну.

3) Стиль дизайна определяется плотностью запросов реализации новых фич (требоаний, реквариментов), выше плотность - больше паттернов/компонентов. Чем размельченнее код, тем время внедрения новых фич в код более линейно, или даже еще лучше. Характер заказчика играет в этом роль.

4) Граница между разными сервисами проходит там, где можно выделить независимую и заменяемую часть логики.

5) "потому что ты не знаешь непосредственно, какой конкретно код будет исполняться. Тебе сначала нужно проверить, какие реализации существуют в интерфейсе, а затем разобраться, какая конкретно применяется во время исполнения." - интерфейс вводится для того, чтобы "не знать", какая будет реализация - важен контракт использования.

1 - в статье не описано, какие задачи должна решать написанная система, но возможно что юнит-тесты им и не нужны. Для тестирования юзер флоу зачастую достаточно api тестов, а юнит-тесты используются для покрытия редких участков сложного кода, содержащего алгоритмические вычисления, а также в качестве документации для валидации входных данных.

Удивительно, сразу после написания комментария пришло письмо от М.Видео - Выписка по бонусному счету.

НЛО прилетело и опубликовало эту надпись здесь
В обычном ООП тестируются классы с длинными методами, класс имеет значительную логику (вплоть до класса World в вырожденном случае) и кучу данных внутри. А какие юниты вы выделяете в ФП? Между интеграционными и юнит тестами не всегда есть четкая разница, и юнит тест как в ООП будет тестировать сразу целое дерево вызовов в ФП или даже модуль.

интерфейс вводится для того, чтобы "не знать", какая будет реализация - важен контракт использования.

Судя по тому, что там далее, тут просто интерфейсы используются неправильно, просто чтоб было. Причём сигнатура конкретной реализации влияет на контракт (!). Цитата, собственно

Кроме того, интерфейс — это ещё один файл, добавляемый в проект, актуальность которого нужно поддерживать при изменении сигнатуры конкретной реализации.

Налицо нарушение dependency inversion

Изменение фичи может потянуть за собой изменение и реализации и контракта, все верно.

При чем тут фича? Если при изменении конкретной реализации нужно менять интерфейс и остальные реализации, то проблема в том, что интерфейс выделен плохо.

Обычно изменения контракта приводят к изменению реализации, а не наоборот

Ну это терминологический вопрос.
У пользователя был телефон, а теперь добавился емейл.
Нужен он для одной фичи с отправкой обратной связи, но повлечет за собой и изменение контракта создания пользователя и возможно изменение контракта отправки обратной связи.
Можно считать это изменением контракта (нужны новые данные), можно не считать (та же задача отправки обратной связи по запросу), но интерфейс изменится.

Ну Вы привели пример изменения контракта.
А я скорее про кейс, когда Вы решили добавить в публичный метод реализации логгера, которая пишет в файл, новый параметр в духе "режим открытия файла" (типа для чтения\для записи и т.д.). Очевидно, что тут изменение специфично для реализации, но нужно будет менять контракт и все остальные логгеры, даже которые не пишут в файл.

Ну, и так и так бывает, вопрос то сложный.

Даже опытным специалистам бывает не просто определить, часть ли это контракта и где границы коспозиции.
Но самое главное, Я бы сказал, даже не это.
Регулярно бывают ситуации, когда контракт сформулирован правильно, с этим все согласны, а потом прилетают такие изменения в требованиях, которые никто не предполагал.
Ну и текущие абстракции никто не отменял ведь.

Я согласен с тем, что Вы пишете, я не согласен с исходной формулировкой

Кроме того, интерфейс — это ещё один файл, добавляемый в проект, актуальность которого нужно поддерживать при изменении сигнатуры конкретной реализации.

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

Переводчик вам вряд ли ответит на вопрос к автору - они работают в разных фирмах и вряд ли общаются напрямую. В заметке есть ссылка на следующий пост автора, посвящённый именно тестированию:
https://betterprogramming.pub/quit-unit-testing-classes-and-use-a-behavior-oriented-approach-306a667f9a31
Кратко - на юнит тестирование забили большой болт, но, вроде как, выстроили супер-дупер систему интеграционного тестирования. По мне - есть риск, что когда аторы супер-дупер системы интеграционного тестирования переместятся на более зелёные пастбища, она заржевеет, и следующий набор сотрудников будет или её переписывать, или таки-восстанавливать возможность юнит-тестирования. Моё персональное мнение, ни в коей мере не претендующее на близость к реальности, так красиво (с картинками!) изображённой автором.

Очень хорошая статья. А есть какая-нибудь книга про шаблоны, паттерны и про то, когда их применять не надо?

https://refactoring.guru/ru хороший ресурс

Кто-то: Как мы избавились от 80% своего кода, повысив скорость разработки и уменьшив количество ошибок

Я: Пытаюсь написать хотя бы пару строчек когда.

Вот когда, тогда.

Как мы избавились от 80% своего кода, повысив скорость разработки и уменьшив количество ошибок

Поздравляю, отличные результаты! Тоже радуюсь сокращению своей кодовой базы

Недавно мы фронт-энд (react) рефакторили -- буду статью писать о наработках. Кратко: выкинули более 1000 строк кода, сократили пространство для появления ошибок

Ну слабое связывание просто надо уметь применять. Зачем кидать интерфейс в конструктор не абстрактного класса? Яркий пример php doctrine. В конструктор EntityRepository передается EntityManagerInterface, хотя соответственное приватное свойство класса и его геттер помечены только как EntityManager. Что они там собрались связывать в таком виде? Причем IDE нотации с типами EntityManager не видит и когда ты хочешь ткнуть в EntityManager::transactional из своего кода, то попадаешь просто в интерфейс вместо реализации.

Вывод прямо напрашивается - оставьте интерфейсы для абстрактных классов. Код от этого только понятнее станет. Бесит, когда хочешь посмотреть код, а попадаешь в "рыбы".

Т.е. контракт может использовать только абстрактный класс?

Может EntityManagerInterface создан потому что EntityManager можно задекорировать?

Так посмотрите сколько там методов в EntityManagerInterface, заколебется декорировать EntityManager (вы ведь не через __call собираетесь делать это) да и ради чего? Кто-то поблагодарит за пачку такого кода, который невозможно понять как работает без дебаггера?

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

80 это много. По картинке так вообще 90% сократили.

1. Слишком мелкая детализация ответственностей

Ерунду написали об SRP. Принцип не о том, что нужно дробить весь код на атомы, а о том, что у одного модуля (функции, класса или файла с исходным кодом) не было разных интересантов для внесения изменений.

так это возможно только если код раздроблен до состояния атомов

Не совсем так. Функция из одной строчки может нарушать SRP, а класс из тысячи строк может не нарушать. Дробление на атомы это другой принцип и Мартин об этом говорит в книге "Чистая архитектура":

Примером нарушения принципа может быть утилитная функция

String toPrettyString(Person person)

Которую используют для преобразования Person в строку (имя + фамилия например). Функцию используют при выводе в лог и при построении отчетов. В один прекрасный момент в логах захотели вывести еще и UUID пользователя и поменяли функцию, а следом поехали все отчеты, которые её использовали.

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

>Принцип не о том, что нужно дробить весь код на атомы

Есть такая странная книга "Мартин Р. - Чистый код. Cоздание, анализ и рефакторинг". Там английским по белому настоятельно рекомендуется: делать методы по 2-3 строчки(!), классы по полэкранчика, switch-и упаковывать абстрактные фабрики, всё максимально выносить в переменные классов, чтобы у методов стало по 0-1 аргументу (2++ нини) и тому подобное... В итоге "чистый код" в три раза длинее "нечистого" )

Причём книга написана в соавторстве с вполне уважаемыми прогерами, но читаешь и волосы везде шевелятся...

Мне очень интересно, где-то есть конторы, где пишут код по Мартину... чтоб резюме туда случайно не отправить )

С таким кодом очень легко работать. Все абстракции и зависимости четко определены и ясны. Любые изменения легко даются или по принципу open/closed из SOLID или легко рефакторятся, чтобы не впиливать какие-то костыли.

Работать может и легко, а вот читать сложно.

Ну правда, если бы от этого была бы одна польза - ок, но чрезмерная декомпозиция крайне затрудняет чтение и понимание кода, не добавляя пользы.

Это все верно только для микросервисов.

Для средних сервисов это - копипаст и лапшекод. Да, плохая абстракция хуже копипаста, но если она грамотная это ок, даже если можно и без нее.

Много классов, мало классов, это такое себе мерило. Я видел обратную сторону, написали в 10 строчек сервис, и потом заюзали его в 100 местах. А потом поняли, что получился бетон.

Осознанный нейминг + четкие границы контекстов + нормальная модульность

"Совершенство достигается не тогда, когда уже нечего прибавить, но когда уже ничего нельзя отнять." (Антуан де Сент-Экзюпери)

Удав сьел слишком много зверюшек. Обезьяна:
- Давайте удаву отрубим голову ?
- Да нет, это очень жестоко !
- Давайте отрежем удаву хвост ?
- Хорошо..
- .. но по самые уши!

Мне кажется, что в конторе персонажа Jonas Tulstrup удаву таки отрезали хвост по самые уши

Выглядит неплохо, но чутка переборщили. Репозиторий лучше выделять в отдельный клас. Сегодня у вас одна БД, завтра другая. Очень часто встречается такая ситуация.

можете пример привести? по-моему БД меняется очень редко

В unit-тестах меняется на in memory представление :)

Я имел ввиду, что во многих проектах за время разработки и поддержки находятся модули в которых приходится поменять хранилище. Причины могут быть разные: скорость, безопасность, стоимость, и т.д.

Рано или поздно это происходит, и вот тогда отдельный слой репозитория очень выручает. Намного проще изначально все хранилища держать отдельно от остального кода, чем потом разгребать.

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

Есть конкретный пример из недавнего - встроенный entity manager по скорости не вывозил - переписали на чистый SQL. При правильном разделении в таком случае затрагивается только инфраструктурный слой, слой приложения не меняется, т.к. интерфейсы остались те же. А, казалось бы, БД та же.

Сложно воспринимать описанное как сферического коня в вакууме (без контекста реальной системы и того, что она решает), но многое выглядит из серии плохих советов, особенно, что нарисованы абстрактные картинки, где нет понимания ни логики ни качества сделанных интерфейсов\абстракций.

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

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

Итого, возникло ощущение, что оптимизации были по сути от не понимания\умения использовать абстракции\паттерны\тесты, где по сути - ой да зачем нам это все надо, соберем все в 1 файл и поехало, работает - не трогай.

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

Такая практика (1 класс = 1 файл) была популяризована Джавой (где это ограничение рантайма, т.к. исходный файл компилировался в файл типа .class 1-к-1), и было скопировано в PHP, C# и т.п., но по сути ничего плохого в этом нет, если у классов сильная связность и они всё равно меняются вместе. Подход "несколько структур в одном файле" поощряется в Go.

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

Ну прям не уверен, что даже в данном примере, классы меняются одновременно.

>Когда смотришь код по файлам пооекта ожидаешь где и что лежит, а тут опана, в контроллере еще какие-то классы лежат.

Есть и обратная ситуация - алгоритм оперирует вложенной иерархией объектов, где часть не имеет смысла без остального (какой-то агрегат, например, или алгоритмическая структура данных), а в итоге всё это размазано по 10 файлам, по которым приходится прыгать туда-сюда, чтобы понять, как это работает. Нужно смотреть по ситуации. Да, в контроллере сразу не придумать, зачем там могут быть лишние классы. Даже в той же Джаве были придуманы "вложенные классы", чтобы можно было несколько классов вложить в один файл, т.к. потребность есть. В Go я часто использую такой паттерн: например, есть сервис, он занимается чем-то своим, но где-то в одной из функций ему нужно делегировать работу в деталь реализации, которую я скрываю за интерфейсом, и если у этого интерфейса потребитель только один (этот единственный сервис, т.к. довольно специфичная одноразовая штука), то я кладу описание интерфейса в тот же файл, что и сервис. Не вижу, в чём тут дичь - сразу в одном месте видно что это такое, я не захламляю проект файлами для мелких интерфейсов, получается такая локализация в одном месте.

Думаю, что подход актуален только в определенных языках и логике, в Go\Rust это по плану является нормальным, т.к. сами по себе языки задуманы именно так, но для предположительно языка из статьи, там явно не этот случай.

Вероятно там что-то вроде PHP\C#\Ruby и для этих языков закидывать классы в один файл, плохой подход. Иначе имеем индусский код, когда в одном файле у нас смешана логика из разных уровней приложения

Бывают чисто утилитные классы, которые нигде больше не нужны, например, чтобы между методами прокидывать несколько значений. Для таких случай придумали кортежи, но для большей типобезопасности и читаемости можно описать небольшую структурку - в том же файле, т.к. больше нигде не нужно. Смешения разных уровней нет, как и в моем примере с интерфейсом. Не знаю, по-моему так размазывание логики по микрофайлам бОльшая проблема, чем страх положить в один файл несколько классов (в языках, где это возможно). В первую очередь в приоритете читаемость и удобство навигации. Разделение на микрофайлы никак не защищает нас от смешивания разных уровни логики, скорее только скрывает реальные проблемы, т.к. при размазывании по файлам немного теряется общее понимание работы кода.

вот мы и дошли до сути - эта статья просто и ясно говорит java это оверинжиниринг как правило. а вот go это nodejs express framework написанный для сишников и это типа тру.

Зато масштабируемый в процессе от нагрузок

Согласен. Много файлов ≠ сложный код. Разве будет хуже поместить файлы Request и Response в папку посвященную их фиче (вместе с файлом контроллера)?

ты сразу же видишь фичу целиком без необходимости прыгать между файлами.

Неужели лучше скролить файл с несколькими классами?

>Неужели лучше скролить файл с несколькими классами?

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

легкорасширяемую систему

А оно надо? Для микросервисов-то? Я понимаю, для чего это делать в монолите, без этого ад и израль. Но микросервис. Написал и выкинул. Иначе получится не микросервис.

юнит тесты, вызывало аналогичные эмоции

Как говорится, юниттесты показывают как хорошо работают ваши моки. Интеграционные чаще полезнее в конечном итоге.

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

Ну вот в Питоне так делают, в Го тоже. Как писали выше, особого смысле нет файлы плодить. Историческое наследие джавы.

Поинт автора в том, что инструмены надо применять тогда, когда они нужны, а не заранее.

Мой поинт в том, что концепции меняются, а методы работы - нет. И никто не задумывается, почему методы были придуманы такими.

А оно надо? Для микросервисов-то? Я понимаю, для чего это делать в
монолите, без этого ад и израль. Но микросервис. Написал и выкинул.
Иначе получится не микросервис.

Я не поклонник микросервисов, так что опытом подкрепить не могу, но как по мне, любую часть проекта надо проектировать нормально, нет ничего более постоянного, чем временное, лучше один раз сделать хорошо (не сильно больше времени требует), чем потом страдать на поддержке, когда автор уже уволился\забыл что и как, а бизнес требует расширения логики.

Как говорится, юниттесты показывают как хорошо работают ваши моки. Интеграционные чаще полезнее в конечном итоге.

Не соглашусь, как раз таки интеграционные показывают больше работу моков, чем юниты, в юнит тестах можно и нужно покрыть как можно больше сценариев работы этого класса. Потом взять модульные тесты и прогнать бизнес логику через них (основные критически важные), а вот интеграционных нужно всего пара штук, для понимания, что система в целом работает контракт выполняется (в режиме черного ящика).

Ну вот в Питоне так делают, в Го тоже. Как писали выше, особого смысле нет файлы плодить. Историческое наследие джавы.

Делать то это можно много где, не скажу на счет джавы, не работал с ней, но в PHP \ Ruby тоже встречаются, когда такое пытаются делать, но это из разряда плохих подходов. Не вижу ничего плохого в кол-ве файлов, если их слишком много - вероятно, что скорее с кодом, что-то не так. На мой взгляд в разы удобнее, когда каждый класс выделен в отдельный файл. Как и с тестами для них же - 1 файл на класс, 1 тест на этот класс.

Мой поинт в том, что концепции меняются, а методы работы - нет. И никто не задумывается, почему методы были придуманы такими.

С этим соглашусь, но как раз в том, что мало кто задумывается о причинах возникновения кроется вся суть.

Делать то это можно много где, не скажу на счет джавы, не работал с ней, но в PHP \ Ruby тоже встречаются, когда такое пытаются делать, но это из разряда плохих подходов. Не вижу ничего плохого в кол-ве файлов, если их слишком много - вероятно, что скорее с кодом, что-то не так. На мой взгляд в разы удобнее, когда каждый класс выделен в отдельный файл. Как и с тестами для них же - 1 файл на класс, 1 тест на этот класс.

Это на ваш взгляд. В Python это достаточно распространённый подход, в ядре интерпретатора и клиентском коде используется повсеместно. Сама объектная модель и схема работы импорта кода из модулей способствует созданию больше одного класса в файле. Рекомендую вам ради интереса попрактиковаться в написании кода на Python и вы сами поймёте насколько это естественно и удобно.

Ещё в питоне можно экспортировать то, что нужно, и файл там в обращении как класс. Просто в РНР и подобных ЯП не хватает вложенных классов, как в Java, которые бы покрыли редкие случаи удобства классов в одном файле.
НЛО прилетело и опубликовало эту надпись здесь

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

Лучше прочитать один файлик на 300 строк, чем 10 файликов на 50 строк каждый.

В большинстве случаев сразу читать все не нужно, а только конкретную ветвь реализации. Вот тогда эти 300 строк начинают очень сильно отвлекать.

Вероятно, когда вы наизусь знаете все эти "ветви" (что за ветви?).

А когда вы только ознакамливаетесь с кодом, или когда лезете что-то поменять, то это очень сильно мешает. Во всем должен быть баланс.

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

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

Выберите слоеную/хексагональную архитектуру, запилите генератор бойлерплейта, и будет вам щастье.

Сразу в любом проекте будет понятно из чего он состоит и куда смотреть в случае чего.

Я так представляю в скором времени у них что бы понять что тут в сервисе происходит нужно будет читать тысячи строк, вместо того что бы пройтись беглым взглядом по абстракциям и файликам.

Хотя сложно сказать что за абстракции у них там были что убрав их они убрали 80% кода. Сколько там строчек кода занимает интерфейс репозитори/хэндлера/сервиса?

Судя по картинкам, ребята отказались от грамотного DDD и CQRS. Выглядело громоздко, если только не требовалось по началу. Ради CRUD такого делать точно не стоило, но сложную бизнес логику в новой модели писать будет явно сложнее.

Переделали (микро)сервисную архитектуру в монолитную. На самом деле, на ограниченном домене, очень интересное решение, просто учитывая нынешние тренды на распределенность, морально сложно к этому придти)) но молодцы, круто)

мы перенесли запрос к базе данных в Repository непосредственно в Event Handler, которому он нужен

Честно говоря стремное решение. Чему меня научили несколько лет разработки всякой шняги, так это то что абстракции нужны и придуманы не просто так. *пусть не в этом случае а в другом сферическом* вдруг у вас меняется база, меняется ее структура.. и тут вам надо вмешиваться в логику толстого event handler. Вот тут какбы разделению ответственности самое место, но его оттуда почему-то убрали. На мой взгляд отрефакторили до состояния на зло маме отморожу уши.

Первое, что приходит на ум из эффективнейших решений - перестали кодить, и вот, - результат! Шутка черный йумор, но жизненно.

И снова убеждаюсь в принципе: "Истина где-то посеридине".


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

Кроме того, интерфейс — это ещё один файл, добавляемый в проект, актуальность которого нужно поддерживать при изменении сигнатуры конкретной реализации.

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

Мне почему-то кажется, что вы уже говорите "гоп", но еще не перепрыгнули.

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

И первым этапом рефакторинга как раз было массовое упрощение по принциам чистого кода и с внедрением единого нейминга по глоссарию проекта. Тоже выкинули два камаза хлама. Но кажущаяся когнитивная простота кода вовсе не привела с собой скорость разработки и поддержки. Не вдаваясь в подробности проекта могу сказать, что на добалвение нового "плагина" как тратилось 2-3 недели - так и продолжало тратиться.

И тогда начался второй этап рефакторинга. Когда на уже наработанной кодовой базе с хорошей структурой кода и сквозным неймнгом стало видно, где реально просятся обобщения и паттерны. В основном провели рефакторинг по SOLID, где-то внедрили CQRS и event-driven architecture. И вот тогда я поставил свой личный рекорд, когда написал новый "плагин" за 2 рабочих дня.

Думаю, ваша история еще не окончена.

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

Какое то очень своеобразное понимание и реализация принципа solid. Если вы столкнулись с подобной ситуацией, то вы явно что то сделали не так.

Solid существует для того чтобы придерживаться реализации при которой код внутри класса имеет high cohesion и low coupling с внешним миром.

Кроме того, наличие классов, меняющихся только по одной причине, часто не даёт реальных практичных преимуществ. На самом деле, внесение изменений в классы, выполняющие несколько действий, часто даёт разработчику гораздо больше контекста, упрощающего понимание изменения и его влияние на окружающий код.

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

Больший контекст при реальной работе не добавляет абсолютно ни каких преимуществ. Откройте любой код на 4т строк и я уверен вы поймёте о чем я говорю. Человеческий мозг очень таки ограничен в своих возможностях.

Понимание Изменения влияния на окружение будет происходить только при учёте анализа точек вызова и ни как иначе. Вне зависимости от размера файла.

Тем не менее, почти все шаблоны имеют недостаток: они повышают структурную сложность и снижают согласованность кода.

Программное обеспечение всегда сложно. Со временем его сложность только растет. Сложность нельзя просто взять и убрать. Ей можно только пытаться манипулировать.

Любой шаблон - это агрегированный опыт многих поколений людей по стандартному решению стандартных ситуаций через которых сложность реализации переносится на уровень сложности архитектуры.

Множество сервисов реализованных в рамках одной архитектуры УВЕЛИЧИВАЮТ согласованность и консистентность кодовой базы. Упрощают понимание кода соседнего сервиса и таким образом значительно облегчают работу по их поддержке и модификации. Облегчают найм людей способных понимать стандартные решения. Я надеюсь что в индустрии таких людей большинство.

Сюрприз но то что вы сделали со своей кодовой базой тоже будет шаблоном пусть и комплексным. Любой новый разработчик будет понимать что происходит на проекте изучив один ендпоинт при условии что все остальные выполнены в рамках той же архитектуры.

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

Полностью поддерживаю.

Отдельной фиче не требуются слабое связывание и интерфейсы для элементов, которые находятся внутри неё, потому что такое слабое связывание имеет свою цену.

Казалось бы верное утверждение, но на самом деле фичи бывают разных 'объемов'. Можно легко написать фичу на ещё 4т строк. Слабое связывание помогает при тестировании фич в отсутствии изначальной системы. Позволяет разобрать фичу на части и тестировать их отдельно. Позволяет разрабатывать ее разными командами людей. Достаточно согласовать интерфейсы. Если вы будете часть фич делать 'побыстрому', а часть как полагается исходя из вашей стандартной архитектуры, то очень быстро придёте к ситуации полного бардака. Потому что, сюрприз, программисты решили что можно и так. А если спросят то всегда сослаться на сраки и дедлайны. И вообще они не виноваты. У вас везде так фичи написаны. Это ваша новая архитектура.

Слабое связывание, и в особенности интерфейсы, снижают согласованность кода и усложняют ориентирование в нём, потому что ты не знаешь непосредственно, какой конкретно код будет исполняться. Тебе сначала нужно проверить, какие реализации существуют в интерфейсе, а затем разобраться, какая конкретно применяется во время исполнения.

Мне кажется что я не так понимаю вашу 'согласованность кода'.

Если у интерфейса есть множество реализаций то как минимум нужно понимать для каких случаев они нужны. Опять же мы понятия не имеем что за интерфейсом скрывается. Да и вся суть собственно в том чтобы и не задумываться об этом. Чтобы инженер фокусировался на важной для него задаче, а не копался в потрохах какой то конкретной реализации какого то конкретного интерфейса. Зачем? Альтернатива этому (шаблон стратегия) будет полная реализация всех альтернатив на месте ( внутри текущего файла) и свитч для выбора нужной на месте вызова. По вашему это действительно упростит чтение и понимание?

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

Мне нужно подготовить по заглушке на каждый вызов метода имеющего внутренний вызов библиотеки? А если я хочу проверить метод с разными данными, значительно влияющими на поток выполнения?

Method(input)

a = lib.method_1(input);

if a

then b = lib.method_2(input);

else b = lib.method_3(input);

Сколько заглушке для библиотеки мне нужно подготовить чтобы протестировать все пути исполнения? Это действительно экономия? Тащить в проект фейковые либы в проект. Между прочим их тоже кому то придется поддерживать. И как раз в этом случае сделать это будет сильно сложнее. Потому что их будет много и для каждой конкретной нужно чётко понимать где и как она используется. И это без учёта возможных кодов ошибок или же исключительных ситуаций которые тоже нужно тестировать.

Вы действительно думаете что не стоит закрывать это интерфейсами?

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

Прозвучало так : мы создали себе проблему, а потом героически ее решили. Решили что это не проблема и просто удалили тесты.

Ранее вы описывали особенность solid - закрытость к изменениям и открытость к расширению. Если бы вы действительно этого придерживались то подобные проблемы с тестами происходили бы у вас крайне редко.

— серьёзно рассмотрите возможность перехода на библиотеку-заглушку, позволяющую имитировать конкретные классы
— Мне нужно подготовить по заглушке на каждый вызов метода имеющего внутренний вызов библиотеки? Тащить в проект фейковые либы в проект.

Подразумевается не конкретная библиотека-заглушка, а библиотека, позволяющая делать заглушки. В оригинале: "seriously consider switching to a mocking library that allows mocking concrete classes".


Сколько заглушек для библиотеки мне нужно подготовить чтобы протестировать все пути исполнения?

Поэтому одну, так же как и с интерфейсом.

Подразумевается не конкретная библиотека-заглушка, а библиотека, позволяющая делать заглушки.

По всей видимости вы правы.

Только вот совет все равно выглядит вредным. Стабать конкретный клас осмысленно только в случае когда вы не имеете доступа к самому классу (не являетесь владельцем кода) и он действительно не имеет интерфейса. Кроме того этот способ имеет серьезные ограничения.

Warning: Substituting for classes can have some nasty side-effects!

For starters, NSubstitute can only work with virtual members of the class that are overridable in the test assembly, so any non-virtual code in the class will actually execute! If you try to substitute for a class that formats your hard drive in the constructor or in a non-virtual property setter then you’re asking for trouble. (By overridable we mean public virtualprotected virtualprotected internal virtual, or internal virtual with InternalsVisibleTo attribute applied. See How NSubstitute works for more information.)

Зачем снимать сиденье с велосипеда мне не понятно. Вы на нем может быть и не сидите, но люди привыкли к другу.

В C# может и так, а например в PHP можно замокать класс целиком. Делать так имеет смысл всегда, если интерфейс в остальном коде нигде не будет использоваться.

Делать так имеет смысл всегда, если интерфейс в остальном коде нигде не будет использоваться.

То есть все таки далеко не всегда. И как я показал своим примером далеко не везде. Это и есть моя основная идея.

Даже если разброс 50/50 использование небезопасного подхода приведет к кому что 100% нужно задумываться лишний раз о том а как лучше реализовать в этом конкретном месте вместо того чтобы придерживаться безопасной реализации всегда.

Да. В каком то смысле это вкусовщина. Но на мой вкус это попахивает больше ленью инженера. Потратить 5 минут на создание интерфейса в процессе работы не вносит значительного импакта на время разработки. Интерфейс с парой методов не создаст никому проблем. Убережёт вас от ненужной контекстной информации. Позволяет делать простые и безопасные тесты (пусть на с# и вероятно где то ещё(скорее всего во всех строго типизированных языках)). И вообще это инверсия контроля и не мне рассказывать зачем это нужно.

Даже если разброс 50/50 использование небезопасного подхода

Каким образом моки классов более небезопасны, чем интерфейсы?


Потратить 5 минут на создание интерфейса в процессе работы не вносит значительного импакта на время разработки.

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


Убережёт вас от ненужной контекстной информации.

Я вот вообще не вижу никакой разницы в контекстной информации между вызовами someVar.method() и someVar.method(). То, что там где-то выше по коду в одном случае тип someVar объявлен без префикса I, не играет никакой роли при анализе кода.

Я вот вообще не вижу никакой разницы в контекстной информации между вызовами someVar.method() и someVar.method()

Наверное имелся в виду code completion. У класса вывалится чёрт знает сколько методов и полей, а у интерфейса - обычно гораздо меньше.

Каким образом моки классов более небезопасны, чем интерфейсы?

Кажется вы плохо читаете мои комментарии.

Дублирую

Warning: Substituting for classes can have some nasty side-effects!

For starters, NSubstitute can only work with virtual members of the class that are overridable in the test assembly, so any non-virtual code in the class will actually execute! If you try to substitute for a class that formats your hard drive in the constructor or in a non-virtual property setter then you’re asking for trouble. (By overridable we mean public virtual, protected virtual, protected internal virtual, or internal virtual with InternalsVisibleTo attribute applied. See How NSubstitute works for more information.)

Понадобилось добавить method_4 в реализацию, которая единственная, и надо лезть еще и в интерфейс.

Для начала стоит хорошенечко подумать: а с какой такой стати вы меняете интерфейс взаимодействия с подсистемой (интерфейс может скрывать не просто класс из вашего проекта но и вызов к внешней системе)? Вероятно вы выбрали неверную абстракцию изначально. Во вторых это вновь нарушение принципа закрытости и открытости. Если этот метод плотно связан с предыдущей функциональностью то его вызов должен быть скрыт за простым, публичным интерфейсом. Если это независимый от предыдущейфункциональности метод то он должен быть выделен в отдельный интерфейс (interface segregation principle).

Теперь задайте себе вопрос : как часто вы будете добавлять по новому методу который нужно будет обязательно дополнительно вызывать в части или во всех точках вызова? Мое личное имхо на основе моего личного опыта - умозрительно редко. Чаще будет происходить сокрытое добавление вызова внутри публичных мелодов (валидации, блокировки).

Понадобилось поменять тип аргумента в method_3, и надо снова лезть в интерфейс.

За вас это прекрасно сделает ide. Только не говорите что вы пишете код в блокноте.

Я вот вообще не вижу никакой разницы в контекстной информации между вызовами

Ниже вам уже ответили о том что внутри класса может быть сколь угодно много других методов и полей не относящихся к текущему контексту. Более того интерфейсом вы подчеркивает какая именно функциональность востребована конкретно тут в отрыве от реализации (которая может быть любой). Это даёт нам право игнорировать эту самую реализацию и фокусировать внимание на текущем уровне абстракции вместо того чтобы исследовать конкретную реализацию. Тк если она фиксирована то она имеет значение, может повлиять на поток выполнения или каким то образом явно мутировать данные (этим в целях экономии памяти могут заниматься приватные методы класса). Конечно, в общем случае, достигнуть подобного возможно и при работе с интерфейсом, но чуть большими усилиями в сравнении с тем чтобы создать конкретную реализацию класса на месте и передать в него по ссылке данные используемые в последствии. Интерфейс не гарантирует безопасности вызова сам по себе, но подталкивает по другому относиться к проектированию компонент. Ещё раз invention of control.

Дублирую

Так вы там говорите про конкретный язык в конкретной версии, а не про "подход". Если моки классов небезопасны в вашем языке, это не означает что небезопасен сам подход в целом. В PHP с этим проблем нет.


Для начала стоит хорошенечко подумать: а с какой такой стати вы меняете интерфейс взаимодействия с подсистемой (интерфейс может скрывать не просто класс из вашего проекта но и вызов к внешней системе)? Вероятно вы выбрали неверную абстракцию изначально.

Нет. Потому что требования поменялись. Потому что в новой версии lib (или тем более внешней системы) появились новые возможности, или поменялись существующие (например исправили баг, что повлекло добавление нового параметра в метод). Потому что нельзя на старте идеально продумать протокол взаимодействия компонентов и никогда за 10 лет развития приложения его не менять.


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

Мы говорим про добавление нового метода в этот простой публичный интерфейс. Например, раньше подключение к какому-то ресурсу закрывалось автоматически, а потом решили, что надо добавить метод Close().


Если это независимый от предыдущей функциональности метод то он должен быть выделен в отдельный интерфейс

Ну так это никак не устраняет ситуацию "понадобилось добавить метод в реализацию — лезем еще и в интерфейс". Пусть лезем не в изначальный интерфейс, а в другой, лезть-то все равно надо. А пока разрабатываем и тестируем, туда могут добавляться дополительные аргументы, меняться их типы или типы возвращаемых значений. И для всего этого надо делать аналогичные изменения в интерфейсе. Это уже не "потратить 5 минут на создание интерфейса".


как часто вы будете добавлять по новому методу который нужно будет обязательно дополнительно вызывать в части или во всех точках вызова?

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


Ниже вам уже ответили о том что внутри класса может быть сколь угодно много других методов и полей не относящихся к текущему контексту.

Лично я никогда не рассматривал это как проблему, но создавать дополнительные сложности в разработке ради того, чтобы подсказки в IDE были другие, мне кажется неправильным подходом. Более того, если они являются публичным контрактом реализации, значит они зачем-то нужны, и есть вероятность, что могут понадобиться и при изменении вашего кода. А с интерфейсом в этом случае появляется то, о чем я говорю — надо лезть в интерфейс и пробрасывать.


Более того интерфейсом вы подчеркивает какая именно функциональность востребована конкретно тут в отрыве от реализации (которая может быть любой)

Когда я ищу баг, мне неинтересно, какая функциональность тут востребована, мне интересно, какая реализация тут вызывается. Потому что баги в работе программы всегда находятся в реализациях, а не в интерфейсах.
Если мне неинтересны детали реализации, я просто не буду в них переходить.


Это даёт нам право игнорировать эту самую реализацию и фокусировать внимание на текущем уровне абстракции вместо того чтобы исследовать конкретную реализацию.

Зачем нам исследовать конкретную реализацию, если мы смотрим вызывающий код? Есть конкретный код someVar.method(); someVar.method2();, ничего не мешает фокусировать внимание на этом уровне абстракции, независимо от того, какой там тип у someVar.


может повлиять на поток выполнения или каким то образом явно мутировать данные (этим в целях экономии памяти могут заниматься приватные методы класса)

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

У нас по факту интерфейсы используются или чтобы скрыть общение между слоями приложения (напр. чтобы работа с SQL не торчала в домене), или если есть общепринятый в команде паттерн вроде вынесения кода проверки прав в декоратор (чтобы не захламлять логику). В остальных случаях можно жить без интерфейсов и использовать класс напрямую. Но есть в интерфейсах свой плюс, что они позволяют сосредоточиться на контракте при разработке взаимодействия компонентов, т.к. внимание не захламляется деталями реализации, но это слабый аргумент, т.к. IDE имеют для этого механизмы, плюс можно просто самому выписать список методов куда-то отдельно (например в draw.io)

. В PHP с этим проблем нет.

Пхп тоже всего лишь язык. Я привел конкретный пример когда подобный подход менее безопасен чем использование интерфейса. Это, как по мне, достаточный ответ на изначальный вопрос

Каким образом моки классов более небезопасны, чем интерфейсы?

Если хотя бы в части случаем метод менее безопасный это значит что он менее безопасный в целом.

Потому что требования поменялись.

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

Потому что в новой версии lib (или тем более внешней системы) появились новые возможности, или поменялись существующие (например исправили баг, что повлекло добавление нового параметра в метод).

Да. Все так. Что изменилось от наличия интерфейса? В худшем случае вы добавите метод в интерфейс руками. Остальное за вас сделает ide. Это не проблема.

Мы говорим про добавление нового метода в этот простой публичный интерфейс. Например, раньше подключение к какому-то ресурсу закрывалось автоматически, а потом решили, что надо добавить метод Close().

Тот самый случай когда вы добавите метод в интерфейс. Это займет больше 5 минут? Как часто вы обновляете библиотеки в своих проектах?

А пока разрабатываем и тестируем, туда могут добавляться дополительные аргументы, меняться их типы или типы возвращаемых значений. И для всего этого надо делать аналогичные изменения в интерфейсе. Это уже не "потратить 5 минут на создание интерфейса".

Ознакомьтесь с возможностями современных ide

При чем тут точки вызова? Я говорю про ситуацю, что при изменениях в реализации надо параллелельно менять интерфейс. Менять реализацию чего-то таким образом, что затрагивается публичный контракт, приходится довольно часто,

Если изменился контракт вы меняете входные данные в точках вызова. Изменения внутри интерфейса происходят автоматически при использовании ide. Единственным случаем ручного изменения могу придумать только добавление нового публичного метода.

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

Все верно. Более того, осмелился бы даже сказать не "довольно часто" а 95% интерфейсов имеет одну реализацию. Иногда есть специальные реализации для дебага и тестов.

Смысл атомарного подхода в том что мы дорожим сложный алгоритм на цепочку простых и независимых. Что бы в каждый момент времени у нас был максимально узкий контекст работы и анализ этого кода был для нас тривиален. Вы будете писать, тестировать, отлаживать один раз, а читать его тысячи. И там где вам не нужно знать конкретно каким методом считается условное 'среднее' вы не будете этот код видеть.

Лично я никогда не рассматривал это как проблему, но создавать дополнительные сложности в разработке ради того, чтобы подсказки в IDE были другие, мне кажется неправильным подходом.

Вы правильно написали. Это ваше личное мнение. У других людей оно может быть другим. Я не вижу сложности в явном выделении сегрегированного интерфейса для класса. Для меня большая проблема видеть конечные реализации на пару экранов которые по какой то причине не были закрыты интерфейсом.

Когда я ищу баг, мне неинтересно, какая функциональность тут востребована, мне интересно, какая реализация тут вызывается.

Представим простую цепочку вызовов:

Controller -> Facade -> BLService -> Repository

Допустим у вас баг в бизнес логике. Вам важно какая у вас реализация чего то кроме BLService? Мне думается что нет. Я проигнорирую абсолютно всю цепочку кроме конкретного вызова. Попасть в этот вызов можно разными путями. Найти реализации через поиск или через дебаг или через логи. Это не проблема. Главное тут что вы так или иначе игнорируете реализацию 80% классов и исследует только ту что вам действительно фажна.

Зачем нам исследовать конкретную реализацию, если мы смотрим вызывающий код?

Зачем в коде оставлять конкретную реализан если она не важна? Высокоуровневый компонент не должен зависеть от деталей реализации конкретного инкапсулированного объекта. Это создаёт сложности для тестирования и расширения.

В рантайме под интерфейсом всегда есть какая-то конкретная реализация, которая каким-то образом мутирует какие-то данные.

Именно по этому проектировать нужно с интерфейса взаимодействия задавая контракт и ограничения на изменения данных. Реализация не должна знать деталей вызова как собственно и наоборот. Когда вы используете конкретную реализацию (даже если один раз в одном конкретном месте) вы потенциально создаёте опасность того что кто будет манипулировать данными (компонента данными овнера или овнер данными компоненты) тем самым нарушая публичный контракт взаимодействия. Например обновлять какие то параметры компоненты напрямую после вызова методов. Просто потому что: ну а чё бы и нет. Я же так сэкономил 5 минут на работе.

Видится мне это все так что все описанные вами "сложности" сводятся к тому что в процессе работы вы не знаете что будете делать дальше, не планируете на будущее и не хотите тратить чуть больше времени на написание тривиального, шаблонного кода.

С одной стороны я понимаю что многое действительно невозможно предугадать, но это не причина не придерживаться стандартных подходов.

Если хотя бы в части случаем метод менее безопасный это значит что он менее безопасный в целом.

Нет, если в языке X нет этих проблем, то по этому критерию оба подхода равнозначны, и можно использовать любой из них.


Но с декоратором вы как раз могли бы легко изменить вызов одного метода на другой или целочку вызовов в одном единственном месте.

Если библиотека не имеет своего интерфейса, или он не такой, какой вам нужен, значит у вас есть класс с этим интерфейсом, который и делает вызовы библиотеки. И тут нет разницы, пробрасываете вы класс или интерфейс, менять вам надо все равно в одном месте.


Что изменилось от наличия интерфейса? В худшем случае вы добавите метод в интерфейс руками.

То, что надо туда лезть и добавлять. Это доавбляет сложности на этапе дальнейшей поддержки, а не только 5 минут при создании, как вы говорили.


Тот самый случай когда вы добавите метод в интерфейс. Это займет больше 5 минут?
Как часто вы обновляете библиотеки в своих проектах?

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


Ознакомьтесь с возможностями современных ide

Да неважно, что там есть в современных IDE. Даже чтобы просто нажать кнопку, надо держать в голове, что надо нажать кнопку. А если не нажал и запустил компиляцию или тесты, они поработали 3 минуты и выдали ошибку, "ой точно, я же интерфейс не обновил", и вот на это ушло еще 3 минуты.


И там где вам не нужно знать конкретно каким методом считается условное 'среднее' вы не будете этот код видеть.

Обычно это надо знать всегда, когда лезешь в какой-то код в поисках бага, иначе просто нет причины в него лезть.


Допустим у вас баг в бизнес логике. Вам важно какая у вас реализация чего то кроме BLService? Мне думается что нет. Главное тут что вы так или иначе игнорируете реализацию 80% классов

Мне она неважна независимо от того, через интерфейс проброшен репозиторий или через класс. Я его проигнорирую в обоих случаях.


Найти реализации через поиск или через дебаг или через логи.

Вот я как раз про это и говорю. Вместо того, чтобы нажать сочетание клавиш для перехода к реализации надо запускать поиск и искать по названию метода, или через возможности современных ide по поиску мест использования, которые этот список строят по полминуты.


Зачем в коде оставлять конкретную реализацию если она не важна?

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


Это создаёт сложности для тестирования и расширения.

В каких-то языках может и создает, а в PHP, который я использую, не создает. В PHP можно спокойно замокать весь класс, для этого не нужен интерфейс.
Такие возможности для расширения в моей практике как правило были не нужны, в 90% случаев используется единственная реализация. А если потом понадобится несколько, тогда и можно заменить на интерфейс, никакой проблемы в этом нет.


Именно по этому проектировать нужно с интерфейса взаимодействия задавая контракт и ограничения на изменения данных.

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


будет манипулировать данными (компонента данными овнера или овнер данными компоненты). Например обновлять какие то параметры компоненты напрямую после вызова методов.

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


вы не знаете что будете делать дальше, не планируете на будущее и не хотите тратить чуть больше времени на написание тривиального, шаблонного кода

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

Вокруг сложности крутятся все приёмы, рассмотренные в посте, включая и сложность всасывания кода в мозг. А что такое сложность? Количество элементов в системе? Количество связей между элементами, которые, сами по себе, тоже являются элементами? Количество уровней моделей, т.е. система описаний из моделей разных уровней, в которых объекты, являющиеся экземплярами классов метамодели, являются классами модели рассматриваемого уровня? И нет никакого ограничения на количество этих уровней. И самый интересный уровень – уровень самоописывающейся модели. Это единственный способ преодолеть необходимость языка (модели) для описания другого языка (модели).

В целом, всё это описывается информационными множествами. А у множеств есть одна принципиальная особенность: на них действует принцип Дирихле. Этот принцип – закон сохранения, только в области логики и информации, а не энергии и вещества. Т.е. все приёмы, рассмотренные в посте, зачастую, трансформируют одни элементы в другие, а объём сложности сохраняется, меняя лишь своё распределение по конструкции. Стало меньше файлов? Увеличился объём кода в методах, увеличилось количество методов. Убрали уже сделанные интерфейсы? На каком-то этапе эволюции системы их придётся вернуть. А это уже затраты, пропорциональные (2*создание+удаление).

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

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

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

Причём единственное реальное назначение кода – быть выполненным на исполняющем устройстве. Никакого другого назначения у него, по происхождению и цели, нет. Однако кто-то навесил на код ещё кучу разных задач, ответственностей, но при этом посвятил огромный первый раздел тому, как не надо распределять задачи, ответственности и описывать их. Почему этот же подход, эти же приёмы не применены к процессу разработки?

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

Да, я веду к тому, что проблема, поднятая автором, находится в средствах управления и описания конструкции и владения ими на уровне каждого мозга разработчика. Не нужно использовать код для передачи знаний и сведений, предназначенных для разработчиков и людей вообще, и тогда масса проблем и плясок вокруг него станут ненужными. А ещё, не нужно устраивать многоуровневые схемы перевода и трансляции кода во время выполнения. И тогда код и поведение системы станут проверяемы на этапе сборки. И не просто проверяемы, а вычислимы до написания кода.

Суть проектирования в этом – вычислить конструкцию, т.е. и её структуру, и её поведение, и её размещение, т.е. все те абстракции, которые их описывают до написания кода. И тогда код начнёт приносить свою главною пользу – будет написан один раз и многократно использован. И потому его не нужно будет перечитывать почти каждому, кто его использует, чтобы вычислить структуру, поведение и эффекты от их изменений: все знания и сведения, предназначенные для людей, будут передаваться иными путями и способами.

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

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

Автор(ы) прав по существу.

Перефразируя принцип "Бритва Оккама", "Не надо плодить лишние сущности (файлы)".

1. В интерпритируемых языках (PHP, Python, JavaScript, ...) чем меньше количество файлов, тем быстрее загрузится приложение (время подготовки и чтения файла всегда большое/медленное).

2. Но и файлы (желательно) не должны быть больше 2-х (двух) экранов стандартного монитора, чтобы обеспечить "наглядность и обзор кода".

В целом, здравый смысл и инженерный подход можно только приветсвовать.

Хорошая статья. Легкие решения проще поддерживать и меньше шансов создать новые ошибки.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий