April 19, 2023

Велосипеды и костыли: история одной идеи

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

Стартапы...pet-проекты... Источник: Яндекс.Картинки

Идея для pet-проекта

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

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

Я хочу качественный продукт

Первое что говорит себе любой разработчик: "Я хочу получить качественный продукт". Почему это проблема?

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

Как только вы хотите делать pet-проект найдите или выделите человека, который будет оценивать результат со стороны пользователя. Скажем, сервер авторизации бесполезен без удобной формы. Функционал пользовательских параметров не нужен, пока UI не учитывает их. И так во всём.

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

Я хочу поддерживаемый код

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

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

Я хочу автоматизировать процесс разработки

Бич красивых реализаций, особенно когда вы работает с языками, которые поддерживают меттапрограммирование. Ни один junior не сможет вам провести полноценный debug макроса и кода, который в итоге попал в программу. Это слишком сложно для среднестатистического специалиста!

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

Я хочу использовать "крутой" стек

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

Итоги моих ошибок

Итогом моих попыток сделать крутой продукт стал перевод всего проекта на NodeJS и фреймворка Nest. Пришёл я к этому после того, как отказался от Julia, Racket и CommonLisp.

Использование Dlang + Vibe.d для требовательного API и Ruby + Roda для не нагруженного API

Dlang - очень удобный язык. Это C++ с удобством Python, не плохим пакетным менеджером, компиляцией и возможностью управления памятью. Одним из главных фреймворков Dlang для web'а является Vibe.d. Главный недостаток, который я смог выявить - дикие утечки при компиляции достаточно простого сервиса (сервис авторизации). Почему так - коллега, который занимался DevOps толком не разбирался и просто докинул ресурсов. Возможно у нас просто кривые руки.

Что до Ruby и Roda - отличный стек, но нам казалось, что он будет не достаточно производительным. А количество строк кода немного пугало (ведь у нас везде были микросервисы, что - для команды из 3 человек мягко говоря зря). Вот с Ruby проблем не возникло от слова совсем.

Я из 2023 года: могу сказать, что Ruby + Roda были слишком не удобны для ветвистой логики. А вот DLang и Vibe.d - прекрасные инструменты. Я и сейчас хотел бы вернуться с Golang на Dlang, единственное что отталкивает меня - менее готовая экосистема для gRPC и отсутствие взрослых ORM.

Отказ от Racket

Lisp, который просто приятно использовать. Источник - Яндекс.Картинки

В одно утро я нашёл достаточно молодой фреймворка Vela для Racket. Понравилось то, что код выходил очень компактным, а сам язык поддерживал прекрасную возможность использовать макросы для генерации исходного кода сервера. Почему-то нам идея это понравилась. Мы хотели описывать декларативно в YAML-файлах под сервера и логики API (если она была на уровне CRUD) с возможностью подключения handler'ов там где нужно что-то сложнее.

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

Начали длительный процесс реализации идеи.

Сразу хочу сказать, что от Racket я отказался исключительно потому, что инструменты выбранные мной просто плохо работали (скажем, пришлось делать доработку драйвера для MongoDB для того, чтобы я мог использовать необходимый мне функционал). Язык Racket является одним из наиболее красивых из стеков с которыми я поработал (и именно в этот момент я пожалел, что не стал использовать Clojure), но количество батареек удручает. Хотя, если бы я не пытался писать универсальное нечто - было бы всё намного проще и удобнее. В свою очередь хочется сказать спасибо всему сообществу языка Racket за отзывчивость. При работе над платформой на основе Racket, MongDB, Vela я получил бесценный опыт, который останется со мной навсегда. Всем советую хотя бы раз попробовать сделать такой продукт.

Я из 2023 года: Если вы и правда хотите сделать проект, а опыта у вас не много - не берите Lisp. Его диалекты включают отдельную экосистему со своими правилами, сложившимися очень давно. И высока вероятность, что ваше представление о том, что надо - расходится с мнением сообщества. А вот если вам интересен в первую очередь процесс исследования или познания - lisp-family прекрасный выбор.

Отказ от Racket в сторону Common Lisp и работа под Woo

Лучший маскот для самого гибкого языка программирования. Источник - Яндекс. Картинки

В какой-то момент я устал бороться с недостатком батареек под Racket и посмотрел в сторону Common Lisp. Требовалось не так много для переноса кода с одного диалекта на другой. Аналог Vela можно было реализовать достаточно быстро самому. В качестве сервера был выбран не блокирующий Woo. В качестве реализации был взять Steel Bank Common Lisp (SBCL).

Начался новый цикл переноса кодовой базы. Он шёл достаточно быстро и просто (благо Emacs в качестве IDE мне нравится, а REPL для CL просто прекрасен). Почему ушли? В момент, когда сервер научился собирать из YAML-файлов базовый сервер я понял, что я просто не найду людей на поддержку продукта. В одиночку - уже не вытягивал. Как раз прошло серьёзное повышение по работе, которое отъело много свободного времени (освоение новых областей ответственности).

Следовательно мне было надо передавать часть работы junior'у, который до этого просто разбирался с базовыми принципами архитектуры. И тут я могу сказать - порог вхождения в Common Lisp выше, чем в Racket. Просто потому, что для Racket есть очень удобная и качественная, современная документация. Даже сам Emacs как IDE стал проблемой.

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

Я их 2023 года: Common Lisp прекрасный язык и на нем можно было спокойно бы оставаться, если бы не вопрос поддержки. Мало того, к 2023 году экосистема начала расти и развиваться благодаря Ultralisp. Но поддержка будет дико дорогой.

Julia и проблемы ФП

Самый приятный python-подобный язык. Источник - Яндекс.Картинки

И после работы над попыткой написать платформу на достаточно функциональном, но в тоже самое время гибком язык Julia могу сказать - мне не понравилось. Julia... привкус python в нём силён на столько, что возникают вопросы, а почему не Python?

Решалась работа junior'a тут просто, кстати. Julia умеет вызывать Python и получать из него результат выполнения кода. Да только мы так и не добрались до этой фичи, т.к. погрязли в куче разных доработок функционального кода. Почему так вышло?

Именно с переходом на Julia и получения быстрого начального результата мы задумались о бизнес-сценариях и поняли, что кодовая база перенесённая с Lisp просто не выдерживает никакой критики. Мы делали продукт красиво выглядящий для программиста-теоретика и отвратительный для прикладника. Решения, которые мы предлагали - было очень сложным, выглядело монструозным. А ещё - именно тут мы осознали на сколько же в бизнес-кейсах удобно ООП. Без него мы были вынуждены придумывать массу сложных обёрток и действий, которые позволяли получить нам нужный результат. В итоге фича которая делается в ООП языках за счёт простого наследования превращалась в новую задачу по продумыванию архитектуры. Не спорю, что возможно просто я плохой ФП программист или начал переносить на Julia код из Lisp "в лоб" (там я проблему наследования легко решал макросами).

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

Я из 2023 года: зря я взялся за NodeJS, надо было оставаться на Julia. Единственный минус был - плохая работа с MongoDB, а нужно было просто перейти на PGSQL или MySQL/MariaDB. Это был лучший вариант, который мне позволил бы сэкономить примерно год (!) рабочего времени.

NodeJS, Raccoon, Nest

Лучший фреймворк для NodeJS. Источник - Яндекс.Картинки

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

Тогда я снова обратился к фреймворку Nest, который когда-то использовал на прошлой работе (тогда он был в зачаточном состоянии) и увидел...они реализовали всё, что мне было нужно и как мне было нужно.

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

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

Я из 2023 года: NestJS слишком спорный. У меня есть стойкое отвращение к TypeScript (он бесполезен и я расскажу об этому когда-нибудь), а сам NestJS избыточен. Руками DI сделать быстрее, проще и понятнее. Собственно когда-то я это уже делал для MoleculerJS. Зря не взял свои же наработки.

Итоги

Итогов работы над этим pet-проектов на самом деле несколько. Какие? Я их разбил для себя по разным годам. И ниже привожу выводу по каждому.

Сама статья была написана в уже далеком 2021 году (а дополнена только сейчас в 2023 и бережно перенесена с Яндекс.Дзен), когда казалось, что реализация "мечты" уже вот-вот, очень близко.

Итоги 2020 года

Закончился 2020 год и мы отказались от своего pet-проекта. Сейчас он трансформировался в работу над 2 более простыми сервисами без замашек на разработку универсальной платформы. С одной стороны - это очень печальный опыт. С другой - мы наелись много кактусов и получили большой опыт в разработки по настоящему сложного продукта очень малой командой.

Выводы которые мы сделали для себя:

  • маленькая команда должна брать монолитную архитектуру, но делать из неё луковицу. Таким образом не будут тратиться ресурсы на связывание микросервисов с тобой и проще выпустить нужный функционал. С точки зрения DevOps монолит - самый простой вариант в принципе.
  • сложные языки (Lisp, Haskell) не просто так не стали популярными. Порог вхождения в них очень высок, в то же время - программистов способных их освоить для написания полезного кода не так много. Беря такой стек - вы обрекаете проект на проблемы с поддержкой и трудностями в разработке (батареек для них в разы меньше).
  • компилируемые языки в любом случае потребуют вменяемый build-сервер, иначе чехарда с контейнерами будет достаточно узким местом. К тому же - в реальных задачах скорее всего вашим узким местом станет канал или СУБД, а не сервер приложения. Количество же запросов дешевле решать горизонтальным масштабированием.
  • не стоит изобретать велосипед на не популярном стеке. Стек не просто так не получил распространения, вероятнее всего где-то есть проблемы и связаны они именно с реализацией бизнес-задач.
  • применимость стека можно понять только при работе над бизнес-задачами. В остальных случаях вы работаете со "сферическим конём в вакууме" и с ним всё будет хорошо.
  • пишите тесты. Только написание тестов позволило нам быстро переносить код со стека на стек и не терять функциональность.

Удачи вам в разработке pet-проектов! Надеюсь эта заметка будет кому-то полезна.

Итоги 2021 года

Почти весь 2021 год прошел под эгидой переосмысления проекта. С основным идеологом проекта мы выкинули все не нужное (а его накопилось очень много, начали заново прокладывать бизнес-процессы). И... я продолжил собирать pet-проект на NodeJS/Nest.

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

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

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

В этот момент на backend появился Clojure и на frontend стал использоваться Svelte/SvelteKit. Да, от них проект в итоге ушел. Но мне этот год дал многое. В частности - позволило по новому посмотреть на архитектуру тем, что я занимался.

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

Итоги 2022-2023 года

В этот момент у pet-проекта появилась настоящая команда разработки. Именно организованная и понятная.

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

За, примерно, месяц я перенес все наработки Clojure на Julia (да, я опять вернулся к ней из-за простоты и превосходного инструментария), а базу сменил MongoDB на PosgreeSQL, так как выбрал для реализации - Genie, приятный фреймворк имевший свою ORM, но не работающий с MongoDB.

Примерно после переноса 2/3 кодовой базы с Clojure был найден backaend разработчик и frontend программист (он подхватил Svelte). Чуть позже к проекту подключился второй разработчик, который хотел освоить Pure C. Проект был бы и сейчас на этом стеке, если бы не одна проблема - новая версия Genie была не совместима с плюшками предыдущей версии мы экстренно начали искать замену.

Попробовали NodeJS - он пошел плохо, так как второй разработчик был Python-программист и NodeJS шел не очень. Тем более - возник спор по поводу использования TypeScript, противником которого я являюсь (это тема отдельной статьи, которую я когда-нибудь напишу. Правда-правда, обещаю!). Параллельно я экспериментировал с CommonLisp и вылился в генератор микросервисов (поставлен на паузу, пока нет времени, каюсь - второй ребенок стал подрастать и теперь у меня два замечательных и любознательных сына, которым все интересно).

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

Я из 2023 года: так же в процессе выбора я честно пытался использовать Rust. Это прекрасный язык с кучей хороших идей и очень наркоманским избыточным синтаксисом. Golang обоснованно стал лидировать - он проще, понятнее, быстрее для большинства программистов. А главное - позволяет получить результат очень быстро. А вот только сервис авторизации на Rust у меня отнял примерно месяц работы (учитываем, что это был первый сервис на нем, да еще я мог писать код по 1-2 часа в день максимум, всего 3 дня в неделю).

Общий вывод

А его у меня нет для вас. Статья - скорее попытка делиться опытом, в первую очередь негативным. А так же - fail story одного проекта, который после 5 (!) лет ментаний принял наконец очертания, понятные бизнес-цели и понятные задачи. И только сейчас я могу сказать - у этой идеи есть будущее. И да, теперь мы даже не думаем о глупой идеи сделать свой Steam. Это будет нечто другое. То, что будет полезно людям. Но о бо всем это позже.

Всем дочитавшим - большое спасибо.