Комментарии + Markdown = 🖤
Одна из самых важных задач в кодинге – делать его понятным для других разработчиков и будущего себя. И для этого существуют 3 основных трюка:
1. Пишите более простые конструкции. If / else, вместо тернарников, for loop вместо filter + map + reduce (именно, когда вам приходится использовать все 3 в комбинации), заранее вычислять предикаты, присваивать их в union type и делать switch, вместо множества вложенный if / else.
2. Выделяйте куски кода в функции. Это кстати один из бонусов декларативного стиля, когда благодаря названию функции, типизация инпута и аутпута вы можете понять "что" происходит, а не читать код, который описывает "как" это происходит.
3. Добавляйте комментарии в код.
И вот про последний пункт, редко можно найти полезную информацию, поэтому я выработал для себя несколько крутых принципов:
1. Comment Driven Development – я очень часто сначала пишу комментарии в функции о том, что она должна сделать, а потом только пишу код. Это позволяет заранее увидить и порешать логические коллизии.
2. Используйте Markdown.
Пример без Markdown:
// First comment const foo = await bar()
if (foo) { // Second comment await helloWorld() }
// Third comment const ping = await ping()
А теперь скажите: First comment и Third comment связаны или нет? То есть, это следующий этап кода или часть предыдущего?
Ответ тут можно только гадать.
А вот если использовать Markdown:
// # First comment const foo = await bar()
if (foo) { // ## Second comment await helloWorld() }
// # Third comment const ping = await ping()
Как вы видите, я обозначаю через Markdown "главы" кода, как если бы это был обычный текст. Благодаря этому можно четко понимать, что Third comment – это новая глава (следующий этап).
Вот таким легким движением руки, ваши комментарии приобретают гораздо больше контекста и понятности для других разрабов
А какие вы используете трюки для написания комментариев?
Комментарии: 16
I
Ivan Zhuravlev
2023-05-29 06:35
Расскажи подробнее зачем markdown заголовки нужны в комментах? Вообще, я использую обычный jsdoc с markdown в части description или examples. Но, заголовки никогда не писал, например, тот же jetbrains умеет jsdoc рендерить из текста в превью markdown и там будет наверное не очень читабельно, если заголовками писать. Но, интересно, поэксперементирую)
I
Ivan Zhuravlev
2023-05-29 06:40
И ещё интересно, какой подход предпочитаешь api-design first vs code-first?
🦾
🦾 IT-Качалка Давида Шекунца 💪
2023-05-30 09:41
А под "трассировкой" ты подразумеваешь трейсы например из Джаегера или как понятие отслеживания?
I
Ivan Zhuravlev
2023-05-30 09:47
А в чём разница 🤔 трассировка это подход, сейчас мы обсуждаем как раз прохождения trace в этом подходе в твоём приложении, а ещё точнее stack trace. Ты можешь создать множество span и объединить их по trace id с учётом parent id и depth level. На каждом span мы сохраняем информацию, вот в твоём случае наша информация в span только на 1 сервисе, а как собрать ошибку в span на 5 сервисе и прикинуть её ещё в 4 spans до 1.
🦾
🦾 IT-Качалка Давида Шекунца 💪
2023-05-30 10:04
Черт, короче, текстом сложно тут до конца понять в чем у нас рассинхрон)
Обычно я делаю так: при отправке сообщения в любой транспорт (http, mq, etc.) проставить ему уникальный id + trace id, так, когда мы дошли до 5-го сервиса, у нас у каждого запроса 5 уникальных id, но всего 1 trace id, когда я из 5-го сервиса возвращаю ошибку через тот же транспорт, она сериализуется и я в span-ах могу увидеть, что запись в сокет содержала в себе ошибку и так от 5-го до 1-го сервиса
Это позволяет мне:
1. Видеть набор span-ов, со всеми взаимодействиями сервисов + I/O, начиная с какого-то триггера 2. Если у меня где-то есть ошибка, я обычно смотрю на графики графаны, собирающиеся из логов, иду смотреть лог, беру из него trace id и иду в систему трассировки смотреть span-ы по этому trace id
И еще раз на всякий случай, возвращаясь к первоначальной теме: если у тебя ошибка бизнес логики (этому пользователю что-то нельзя делать, не нашлась какая-то сущность, нам недостает каких-то данных, валидация и т.д.), тогда мы возвращаем ошибку и отдаем ответ (потому что это не ошибка вовсе), а вот если у нас ошибка, которую мы считаем достойной положить все приложение (отвалилась БД), тогда мы ее выбрасываем
I
Ivan Zhuravlev
2023-05-30 10:16
Да, речь как раз за эту пользовательскую ошибку. Для отладки у нас должен быть в трейсе оригинал ошибки и место её появление. Я вот вижу, что ты пишешь в span где-то у тебя оно создаётся. Но потом думаю, как это будет выглядеть в том же Jaeger, у нас есть родительский вызов и несколько дочерних span в котором 2 ошибки в разных местах. Обычно такого не бывает, всегда одна ошибка и дальше все span передают этот error, так как у нас в том же Jaeger есть возможность написать фильтр error=true и посмотреть все ошибки или уточнить по конкретному сервису.
По поводу графаны и поиска логов, то стандарт протокола open-telemetry как раз и был создан, чтобы объединить 3 сущности: metric, log и trace связывая из между собой одним uuid для обогащения данных в UI по каждому инценденту.
🦾
🦾 IT-Качалка Давида Шекунца 💪
2023-05-30 10:45
Все, вот теперь понятно)
Знаешь, наверное так: на самом деле, реальные "ошибки", то есть те, которым мы разрешаем положить наше приложение, надо выкидывать, а все остальные "ошибки", на самом деле надо рассматривать скорее как "неуспех" / "неудача" и мы их возвращаем. То есть, мы пошли по бренчу "неуспешного" завершения операции.
И вот когда мы говорим про "неуспех", то мне кажется, что неуспехов может быть на протяжении одной цепочки действий (трейса) очень даже много, потому что 4-ый сервис, может абсолютно спокойно ожидать неуспех от 5-го, а дальше просто пойти по другой цепочки логики
Поэтому я не соглашусь с фразой: "Обычно такого не бывает, всегда одна ошибка" – как раз таки на мой взгляд каждый сервис получив ошибку / неуспех от другого решает что он будет с этим делать: создавать новую ошибку / неуспех (а это отдельная сущность) или нормально обрабатывать эту ситуацию
И да, из-за этого в моих спанах будет куча повторяющихся ошибок, потому что в 70-80% процентов случаев мы просто выбрасываем ошибки все выше и выше до точки входа
Опять же, возможно, я так делаю из-за привычки и незнания того, как оно могло бы быть реально лучше и удобнее
🦾
🦾 IT-Качалка Давида Шекунца 💪
2023-05-30 10:57
Я не стесняюсь писать процедурный код и некоторые мои функции могут достигать 1000 строчек кода
Самый живой пример: парсинг данных от устройств (IoT). Дело в том, что там одни свойства данных могут очень сильно влиять на другие и если бы мы делали на каждую обработку функции между которыми передавали эти куски данных (при этом иммутабельно), то рано или поздно окажется ситуация где одна глубокая ветка логики начинает зависеть от другой и приходится очень сильно рефакторить.
Поэтому, я предпочитаю максимум логики оставлять в 1 главной функции, которая парсит, а вот когда я уверен, что ветка независимая или глубина ветки не более 1-2, тогда я выношу в отдельные функции.
Вот в таком процедурном коде и возникает ситуация, где у тебя по 50-100 строчек кода представляют собой некоторую "главу" с "подглавами" и это хочется как-то обзначать.
Для этого, перед главой я пишу комментари "// Extracting sale data", "// Updating state", "// Deduplication" и так далее
Но, если просто писать комментарии, то в один момент встает вопрос: а вот этот комментарий связан с предыдущим или нет?
И когда ты начинаешь к ним добавлять правила Markdown (в основном, заголовки, списки, можно еще bold, italic и т.д), тогда ты намного четче видишь какой кусок кода к какой главе относится и навигация по этой функции становится в разы легче
И да, jsdoc я использую когда просто хочу описать саму функцию, но это скорее о том "что здесь происходит", а не "как это происходит"
I
Ivan Zhuravlev
2023-05-30 10:57
Разделение ошибок действительно правильная мысль. Обычно это делается в местах с пользовательскими gateway. Там мы определяем допустимые для возврата пользователю типы ошибок. Остальное превращаем в общую ошибку.
По поводу разных ошибок в разных span, такое может быть, но это должен быть кейс, когда у нас асинхронно выполнялись несколько действий и каждый из них в своей работе получил собственные ошибки, тогда мы эти ошибки и пишем в их span. А если мы из обоих прокидываем назад по твоей логике, тогда нам нужна на уровне выше ещё одна логика понимания этих ошибок и что отправить дальше. Но, это создаёт более тесную связанность сервисов, что уже противоречит практике малой связанности и разделения ответственности. Так, у нас каждый сервис с которым мы взаимодействием должен знать об этих ошибках сервиса и реализовывать дополнительную обязательную логику, вместо обычного проброса эксепшена выше по стэку.
🦾
🦾 IT-Качалка Давида Шекунца 💪
2023-05-30 11:03
Когда я проектирую систему или большую фичу, то api-design first, а в повседневных тасках могу и code-first
Я поклоник Task-based UI и CQRS (с точки зрения разделения Read и Write моделей), поэтому мои API представляют из себя кучу маленьких запросов на изменение или получение данных, где между ними очень мало переиспользуемых интерфейсов (практически каждый пишется конкретно под какую-то ручку)
Поэтому при проектировании я обычно беру набор своих данных из разных источников (типа БД), беру задачи, которые хочет решать клиент (UI или сервис) и составляю набор этих API, описывая его в виде той или иной нотации (будь то OpenApi, protobuf или TypeScript, если у нас моностэк)
Кстати, вопрос интересный (запилю пост)), а ты что скажешь на эту тему?
🦾
🦾 IT-Качалка Давида Шекунца 💪
2023-05-31 10:26
В целом согласен, но по-поводу связанности сервисов: мы сейчас обсуждаем именно кейс "Request-Response", где наш сервис отправляет куда-то запрос и ждет ответа.
Представим, что это сторонний API (например, Stripe) и мы используем для него SDK. Разве мы не должны знать какие ошибки он может вернуть? Скажу больше, скорее всего, в самом SDK должен быть отдельный файл с перечислением ошибок, которые мы можем отловить и что-то сделать.
Для нашего сервиса неважно (в контексте данного разговора) стучиться он во внешний или в соседний сервис, для него все это API, у API есть описание, в рамках описания должны быть указаны ошибки.
Поэтому как бы то ни было, если мы можем из сервиса вернуть ошибку, то все, кто с ним общаются должны знать о ней (будь то документация, SDK, готовый словарь ошибок в JSON и так далее)
Соответственно, я бы назвал это "натуральным" связанностью, потому что она логически выходит из паттерна Request-Response, который уже сам по себе предпологает знание о точке, куда ты делаешь запрос (и например Event, как противоположность Request-Response)
🦾
🦾 IT-Качалка Давида Шекунца 💪
2023-05-31 10:29
По-поводу дальнейшего проброса: так это и есть то, как например, у себя это реалозвал Go: ты будешь писать foo, err := bar() /n if (err != nil) return nul, err и это как раз таки про то, чтобы не "выбрасывать дальше по стэку", а "явно обработать (даже если это просто проверка на nil) и сделать то, что тебе нужно (даже если это просто return)"
В Zig это реализовано приятнее, где можно сделать try bar() и он сам в случае ошибки просто сделает return из функции, где был вызван bar
I
Ivan Zhuravlev
2023-05-31 10:31
Взаимодействие сервиса со сторонним, это изолированная ответственность. Только этот сервис и будет знать. Но, я понял твой подход, тут каждому свои паттерны.
I
Ivan Zhuravlev
2023-05-31 10:31
Callback так и реализован, в async/await придумали удобный для него подход через throw/catch/finally
I
Ivan Zhuravlev
2023-05-31 10:34
Тут нужно много расписывать. Я оба подхода практиковал и развивал. Везде есть свои нюансы. В целом, придерживаюсь сейчас примерно такой же, как и ты стратегии, всё сильно зависит от ресурсов на входе для принятия решений.
🦾
🦾 IT-Качалка Давида Шекунца 💪
2023-05-31 10:35
Кстати, про callback – да, это самое смешное, что до async / await для нас была норма иметь первым аргументом ошибку, что было ближе к тому, о чем я говорю в посте))
throw/catch/finally можно не писать (забыть или намеренно), а значит это невное поведение, как раз почему я предлагаю возвращать ошибки, где ты не можем не обработать ее, ты прям должен это сделать
Я надеюсь, что появится типа tryasync, который будет возвращать ошибку, если был throw и тогда все будут довольны (только, хрен его знает как тут красивый finally сделать)