Pull to refresh

Comments 30

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

Лучше писать «простой» код. Чем писать неподдерживаемый.

Скрытый текст

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

Статья очень сомнительная. Зачем ровнять между собой TS и Java. Для фронта это все может быть и актуально, а я посмотрю, как вы напишете бэк на java без фреймворка. Нет ничего плохого, что мы привязываемся к фреймворку. Не на сервлетах голых же писать? Рынок бэка на джава - это 90% spring, оставшиеся проценты - это микронавт, кваркус, кора. Мигрировать с одного на другое все равно не будет в большом проекте. Поэтому ничего плохого в этом нет.

Далее, если вы юниты не пишете, это не значит, что их никто не пишет. У меня был проект только из интеграционных тестов (95%). Так вот все тесты проходили за 20-25 минут. Очень эффективно.

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

Про фронт только один абзац, TS я в основном, использую на бэке. Вы правы - на Java я тоже писал на Spring, и старался отметить (в абзаце про ORM), что на Java его использование не только оправдано, но и удобно. Но это автоматически не распространяется на другой язык. А вот бессмысленное использование DI без моков я встречал и на Java и на TS. И на Java статические репозитории работают так же как и на TS. И описанный случай с GUID был с JAVA программистом — т.е. он настаивал, что именно в Джаве так принято (хотя, конечно, это личное мнение в реальности к Java не имеет никакого отношения, просто привычно, что там всё "солидно" и "промышленно").

Возможно вы не поняли про что статья - она не против фреймворков, она против оверинжениринга

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

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

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

Я старался подать платёжные шлюзы как пример уместного DI. Но кажется, что подключение разных реализаций с одинаковым интерфейсом - это редкий случай, не думаю, что ради этого стоит тащить специальную библиотеку обслуживающую инъекции, можно сделать их вручную - код будет только понятнее.
Фреймворк действительно набор паттернов, и некоторые чувствуют себя обязанным применить их все. Я думаю, вы согласитесь, что это излишне.
"народ .. активно суёт вызовы чего угодно напрямую " - я такое видел на Java на Спринге, конечно DI там тоже был - ничто не способно удержать заядлого кулинара от приготовлении лапши.
Мне кажется, что сам DI не делает код ни понятнее, ни лучше, ни более гибким. Это можно сделать с помощью уместного применения DI, я хотел, чтобы статья была об этом.

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

А вот в JavaScript… ORM часто лишён смысла. Запрос к базе и так возвращает массив обычных объектов, а типизировать их в TypeScript можно простым as User[].

Это работает пока запросы относительно простые. Если запрос делает left join со связью 1:М, хочется иметь массив с объектами вида:

[
 {
   login: 'login1',
   name: 'name1',
   comments: [
     {
       text: 'text1',
     },
     {
       text: 'text2',
     }
   ],
 },
  {
   login: 'login2',
   name: 'name2',
   comments: [],
 },
]

А квери билдеры вернут что-то такое:

[
 {
   login: 'login1',
   name: 'name1',
   comment.text: 'text1',
 },
  {
   login: 'login1',
   name: 'name1',
   comment.text: 'text2',
 },
 {
   login: 'login2',
   name: 'name2',
   comment.text: null,
 }
]

Тогда придется вручную мапить, избавляться от дубликатов юзеров и с типизацией разбираться. Согласен, что TypeORM перегружен, но тот же DrizzleORM в 2 раза легче и справляется на ура.

DI активно продвигается как «промышленный стандарт», главный аргумент — удобство тестирования: зависимости можно заменить моками.

Ну не только. DI отлично работает, когда у нас есть несколько сервисов, которым нужно задать одинаковый интерфейс. Например — платежки, которые можно реализовать в виде Стратегии и динамически инжектить.

Чтобы не создавать экземпляры вручную, подключают DI-контейнеры (Spring в Java, NestJS DI в JS). Это наглухо привязывает код к фреймворку, добавляет слои пустых абстракций, делая код неявным.

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

Многие современные фреймворки (Spring Web на Java, NEST и Fastify на JS) позволяют вообще ничего не оборачивать в try-catch — если исключение произойдёт, то фреймворк сам перехватит его, обернёт в объект ошибки и отправит в ответе.

Да, только не все ошибки мы хотим отдавать клиенту. В простых случаях, мы не будем отдавать исключение упавшего запроса к БД, а выкинем какой-нибудь 500 Internal Server Error, при этом записывая в логи проблемный запрос. В более сложных, сторонние сервисы могут отдавать десятки различных исключений, но пользователю мы хотим вернуть что-то общее, типа "Failed to send email" или "Data not found".

вложенные объекты - действительно вариант для ORM. Но, честно говоря, сейчас я так перестал делать. Если запросы разделить (отдельно юзеры, отдельно комменты из вашего примера) то интерфейс реагирует живее. Но если непременно нужно вложенные, то да - ORM, это проще, чем строить запрос с вложенными объектами (что поддерживают не все БД). Может, напишу об этом отдельную статью
про DI с разными реализациями одного интерфейса - согласен. Но я бы делал инъекции вручную - кажется, что такие случаи не настолько всепроникающие, чтобы использовать библиотеку
Ошибки, кажется, тоже проще перехватывать одним мидлваром - и Spring и NEST с Fastify позволяют это делать, именно в нём вы делаете проверки на prod\dev и решаете что именно отдавать клиенту.
В целом у вас совершенно верное восприятие - я не против этих технологий вообще. Я против того, чтобы использовать их там где они профита не дают, а код усложняют

я написал отдельную статью про ORM (как обещал) - https://habr.com/ru/articles/959518/
Помня о вашем комментарии, добавил примеры вложенных объектов на чистом SQL

Ох уж эти джаваскриптезеры, все у них ни как у людей

Откуда берутся все эти утверждения "чаще всего данные отдаются сразу клиенту", "обычно DI не нужен" и т.п.

Запомните раз и навсегда. Нет никаких "чаще всего" и "обычно". Есть разные задачи решаемые системами. Где-то задачи простые и там действительно данные сразу отдаются клиенту и проверяются интеграционными тестами (кстати польза от DI не только для тестов).

А есть задачи сложные (именно сложные с точки зрения бизнес-процессов и предметной области) и там без грамотного моделирования логики, юнит тестов и т.п. код за месяц превращается в неподдерживаемое <censored>, которое ломается от малейшей правки.

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

В статье я преимущественно имею в виду задачу написания REST API серверов.
Старался писать так, чтобы было понятно, что я не против этих технологий как таковых. А против их неуместного использования, и "выбирать подходы следует именно из анализа вашей предметной области". Мне нечего возразить этому комментарию - всё так и есть.
Моя ремарка состоит в том, что не надо использовать технологию, если она не соответствует предметной области и задачам - бесполезное увеличение сложности - это не бесплатно

Сорри за агрессивный тон с моей стороны, просто сильно тригерюсь на суждения "часто/редко", особенно когда это вижу в книгах :)

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

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

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

Конечно, фразы типа "все мы знаем, что большинство..." - это действительно преднамеренное нарушение логики. Поэтому про часто/редко я старался сфокусировать - что это мой личный опыт написания REST серверов. Т.е. мне неуместное использование DI попадалось в чужом коде гораздо чаще, чем уместное. Использовалось оно только потому, что его предоставлял фреймворк. Более того, те кто его использовали не могли толком объяснить, что это и зачем.
В этом смысле Ваш выбор совпадает с моим - если бы я стал использовать DI то скорее всего вручную, без фремворков — это более явно и является сигналом того, что выбор сделан осознанно.

У меня на одном из монолитных проектов, построенном на методологии DDD, абстракции с последующими внедрениями зависимостей нужны в первую очередь не для тестирования (мокирования), как описано в статье, а композиции общей абстракции репозитория приложения из более мелких интерфейсов по фичам/юзкейсам (ISP). Другими словами, я не могу заранее определить реализацию слоя репозитория в проекте, бизнес которого постоянно меняется, а фичи только добавляются. Фичи и домен определяют репозиторий, но не наоборот) Я думаю, громко сказано, что это "только для тестов и моков", в больших проектах это история скорее про модульное масштабирование

Это был REST такой?

Такие проекты не ограничиваются одним REST. Там и REST в том числе. Если вы имеете ввиду CRUD, то это другое дело, там DI оправдывается разве что тестами и моками. Я лишь хотел сказать "главный аргумент — удобство тестирования" - далеко не главный агрумент

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

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

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

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

Или если надо для debug и release сборок разные компоненты использовать - достаточно получить их с селектором - и if будет не в каждой точке использования, и не внутри объекта делить логику, а в di.

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

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

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

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

Хотя я не смотрел его js - может там нет простых решений, хотя учитывая гибкость js - они напрашиваются

Ааа, если вы про Джаву/Котлин, ну ОК - может быть. Там весь спринг построен на инверсии управления и инъекции зависимостей и вся документация крутится вокруг этого. Поэтому, чтобы избегать DI надо применять доп.усилия (вроде не понятно зачем). Т.е. тотальное DI является таким распространённым, что его отсутствие может вызвать доп. вопросы. Код, правда, из-за этого очень неявный - не очевидно что от куда прилетает, что injectable что нет, где надо autowire где нет.
Про js/ts. Типизация в Java номинальная, т.е. требуется непременно потомок определенного класса, а в тайпскрипте как в Go она структурная - поля совпадают по типу - то и норм. Java-файл - это класс, т.е. его надо потом инстанцировать. А в js из файла можно экспортировать что угодно (в т.ч. и сразу инстанс, а не класс) и сколько угодно (инстансов и классов в перемешку), а импорты - всегда синглтоны. Это значит что если вы экспортнёте не класс, а инстанс - то вот вам автоматический синглтон. Поэтому в JS, мне кажется, DI проще делать вручную - это элементарно, соответственно библиотеки DI - не очень понятно что - типа сахара для массового внедрения.
Ну и джава это не только RESTы, в ней действительно больше применений, когда DI уместен.
Т.е. моё мнение - в Spring (Java/Kotlin) так же можно прекрасно сократить количество DI в несколько раз без потери функциональности (это не супер-сила), но из-за тотального доминирования в документации DI-подхода проще согласиться, чем сопротивляться т.к. единственный выигрыш - чуть проще и прямолинейнее код (т.е. тоже не охренеть какой профит).
В JS/TS наоборот - лучше применять только по месту там где он действительно нужен и тем более не таскать библиотеки из-за него

Во первых в своей статье вы критикует DI как подход, а не использование библиотек - это контрастирует с утверждением

Поэтому в JS, мне кажется, DI проще делать вручную - это элементарно, соответственно библиотеки DI - не очень понятно что - типа сахара для массового внедрения

Не важно каким инструментом вы делаете di - если вы его делаете (адекватно) - то это хорошо, а если нет, могут быть проблемы.

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

Если вы не используете модули как источник объектов - то код имеет свойство превращаться в матрёшки A(B(C(Q(Z), Q(Z)), Z, Q(Z))

Ну и синглтон - антирпетрн, так что прямой экспорт объекта может кончится глобальным супер рефакторингом.

В целом когда ваш код выглядит как UserStory(dataProvider), а не UserStory { let dataProvider = DataProvider }

То вы получаете гибкость - заменить dataProvider за 5 секунд.

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

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

"синглтон - антирпетрн" - так почти все провайдеры в спринге по умолчанию синглтоны (@Component, @Service, @Repository...) Или вы имеете в виду, что их синглтонит не конструктор класса а контейнер? Так в данном случае никакой разницы нет - сам Spring использует этот термин (@Scope("singleton") ). А для js/ts это вообще не актуальный вопрос - это решается формой экспорта что по механике как раз и есть контейнерный синглтон как в спринге - в JS для этого не нужны ни отдельные контейнеры ни библиотеки - это встроено в архитектуру языка (а точнее в движок ноды).

UserStory { let dataProvider = DataProvider } - не надо ничего присваивать, надо сразу статические методы вызывать DataProvider.doSomething()

PS. Кажется, будто я оспариваю ваши утверждения - но это не так. Я полностью согласен с утверждением "если вы его делаете (адекватно) - то это хорошо, а если нет, могут быть проблемы". Я просто добавляю две ремарки - 1) адекватных случаев мало, особенно в RESTах, а в JS - микроскопически мало. 2) Бессмысленное использование DI хоть и не ломает код, но делает его неявным, что может ухудшить поддерживаемость. Ну и соглашусь с ремаркой, что в спринге это стереотипный подход

никаких аргументов не имеют.

Вы не договариваете. Никаких аргументов Сейчас. А вот когда они появятся, а рано или поздно они появятся - добро пожаловать в ад. Тем более что на деле как раз репозитории имеют свойство получать в аргументы dao, cacheProvider-ы, а в последствии и мапреры.

сам Spring использует этот термин (@Scope("singleton") ).

Singleton в di задаётся в рамках scope и это в любой момент можно поменять в модуле (я могу поменять singletone на factory заменив 1 слово в коде, сколько вам потребуется поменять, чтобы заменить singletone на factory при прямом обращении? И с di у вас может быть два инстанса singletone в разных скоупах - а это уже не тот сингтон, который невозможно поменять, не пройдя каждое вхождение.

надо сразу статические методы вызывать DataProvider.doSomething()

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

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

Хорошо если у вас все функции чистые - тогда вам и объекты не нужны, и di у вас по сути будет по умолчанию (нет объектов - нет зависимостей). Но когда появляются объекты - всё, di нужен.

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

Это как бесплатная страховка без дополнительных условий - отказываться нет смысла.

Это на спринге он бесплатен (можно и не отказываться, я с этим не спорил). А на ноде нет - на ней, особенно в RESTах отказываться есть смысл - всё что вы описали делается либо без DI вообще, либо точечным DI с нулевыми издержками на добавление из-за отличия архитектуры языка.

Sign up to leave a comment.

Articles