🔥

Тред (@bespoyasov)


А сегодня поговорим о том, как сделать код читаемым и тестируемым ^_^ Расскажите о своих приёмах, как вы улучшаете кодовую базу на проектах? Какие применяете методы, принципы, эвристики? Я пока начну 🧶

Самое простое (и одновременно сложное 😃) — это нейминг. Хорошие и внятные имена для переменных и функций — это очень мощный инструмент. (Первая книга на тему, которую я прочёл — это «Читаемый код» Фаучера: bespoyasov.ru/blog/the-art-o… )

Хорошее имя для сущности: короткое, но полное и описательное. На Гитхабе есть классный чеклист по неймингу сущностей: github.com/kettanaito/nam…

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

Например, внутри модуля пользователя функция create выглядит органично, не дублирует контекст, короткая, описывает действие:
notion image

Но если функцию импортировать и начать использовать, то уже не так очевидно:
notion image

А вот тут — наоборот:
notion image

Годный шаблон для названий можно вот тут посмотреть: github.com/kettanaito/nam…

Шаблон A/HC/LC: prefix? + action (A) + high context (HC) + low context? (LC) В идеале по названию переменной должно быть понятно, функция это, булево значение или что-то ещё.
notion image

Для булевых значений можно использовать префиксы: should, is, has, will, did. Для функций — первым словом лучше поставить глагол действия: get, set, update, remove, delete, push…

Иногда (редко) от шаблона можно отойти, если двусмысленность получается исключить: let mounted = false — тут сложно подумать, что mounted что-то кроме boolean.

Моё любимое — не используйте аббревиатуры, пожалуйста 😃 twitter.com/crocodoyle/sta… (А если используете, обязательно документируйте.)

Всё настраиваемое лучше сразу выносить в конфиги. Пусть даже по началу это будет объект в том же файле. Это и читаемость улучшит, и тестируемость повысит (о тестах дальше).

Используйте паттерны проектирования. Есть замечательная подборка паттернов на том же Гитхабе, очень советую посмотреть: github.com/kamranahmedse/…

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

Паттерны удобно использовать в связке с SOLID. Некоторые из принципов прямо подразумевают какой-то из паттернов. Мы с twitter.com/dex_157 в нашей книжке о принципах SOLID добавляли разделы с паттернами под каждый принцип. ota-solid.vercel.app

Чаще рефакторите код, но без фанатизма ¯_(ツ)_/¯

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

А ещё (самое сложное для меня) надо купировать перфекционизм 😅 Отрефакторить до идеала сложно, а чаще всего не нужно. Пользуемся правилом 20/80 — 20% усилий должны приносить 80% результата. ru.wikipedia.org/wiki/Закон_Пар…

Чтобы рефакторить безопасно, пишите тесты. Физерс в «Эффективной работе с легаси» писал о том, как можно рефакторить старый неповоротливый комбайн: bespoyasov.ru/blog/working-e…

Он предлагает искать швы — места, в которых можно относительно безопасно «распилить» комбайн на части. Покрыть швы тестами, а уже потом начинать рефакторинг. Я пробовал, это и правда работает.

Как найти хороший шов? Обычно шов — это место, где мы можем заменить одно поведение другим: месте соединения модулей. В хорошо написанном коде такие места выделены явно, потому что модули слабо зацеплены.

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

В книжке много техник, как работать с кодом, когда вы уже определились со швом. Типа, как заменить зависимость: - одну зависимость за раз; - определить, какую зависимость хотим поменять; - покрыть шов тестами; - вынести текущий код в отдельный класс; - заменить класс на другой.

Раз уж мы о книгах заговорили, то ещё «Чистый код» Мартина могу посоветовать: bespoyasov.ru/blog/clean-cod… Хотя и считаю, что «Эффективная работа с легаси» шире, глубже и практичнее ¯_(ツ)_/¯ bespoyasov.ru/blog/working-e…

Ещё офигенная и практичная книга — “Debug it!” («Отдебажь это!», простите за кустарный перевод): bespoyasov.ru/blog/debug-it/ Она вся состоит из рецептов, как работать с багами из-за непонятного кода.

Там даже содержание — это уже рецепт! 😃 Глава 1 — Исследовать обстановку Глава 2 — Воспроизвести проблему 3 — Определить причину 4 — Исправить 5 — Как не допустить такой ошибки в будущем ...и т. д.

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

От читаемости к тестированию! 🦸 Используйте TDD 🙂

TDD мне экономит кучу времени. К нему надо привыкнуть, потому что сперва приходится «вывернуть мозги», но он быстро окупается.

С ним: - Исчезает проблема «дополнительной работы» - Писать тесты и рефакторить входит в привычку - Рефакторить безопаснее - Видно сущности, делающие слишком много - API проектируется до реализации, и-за чего становится удобнее

Я недавно делал доклад о TDD на Frontend-crew: - youtube.com/watch?v=1SGb-l… - bespoyasov.ru/talks/podlodka… Там рассказываю об этом подробнее: - как внедрить на проекте; - как использовать; - как сделать тесты проще.

Как упростить тесты при работе по TDD: - Чаще использовать чистые функции - Обращать внимание на зацепление кода - Тестировать только свой код - Использовать удобные инструменты - Потратить время на удобную инфраструктуру

С TDD можно искать пахнущий код. Код пахнет, если: - Тестов слишком много по сравнению с другими модулями - Описание ссылается на несвязанные вещи - Ожидание от теста оформлено невнятно - Подготовка теста слишком сложная - Тест проверяет детали реализации - Тест всегда зелёный

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

Я ещё когда-то написал книжку с примером разработки крестиков-ноликов: bespoyasov.ru/ttt-tdd/ Там показываю, как переходить по циклу TDD, в какой момент приступать к рефакторингу, на что обращать внимание.

TDD можно использовать и при работе с React тоже. Недавно я проводил воркшоп об использовании TDD при разработке React-приложений: bespoyasov.ru/talks/?full#1 Он длинный, около 5 часов, но там я прохожусь по всем основным концепциям, а именно, как тестировать:

- ...Функций бизнес-логики. - Функции, возвращающие случайные значения. - Простые компоненты. - Кастомные хуки, их связь с компонентами. - Работу со стором. - Асинхронные функции и вызовы API. - Пользовательские действия: клик, клавиатура.

Так-с, пора работать! Продолжим во время обеда 😃

Продолжим! Чем же так хорош TDD для тестируемости и читаемости? Он сразу поставит нас в ситуацию, когда сперва придётся думать о тестируемости. Писать код, который будет неудобно тестировать, по TDD — очень сложно 😃

Напомню стандартный цикл разработки по TDD. В нём 3 этапа: - красная зона — на ней мы пишем тест, проверяем, что он падает по нужной причине; - зелёная — пишем реализацию, которая тест проходит; - синяя — рефакторим код и тесты.
notion image

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

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

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

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

А ещё TDD — это единственный способ безопасно (или даже вообще хоть как-то) отрефакторить легаси 😃

Кроме TDD тестируемость улучшит Dependency Injection. Вместо того, чтобы мокать всё подряд, можно использовать DI, и подмешивать во время тестов нужные зависимости.

DI — это не обязательно контейнеры и всё такое страшное, можно использовать кустарный DI через объект с зависимостями в конце. Об этом я тоже недавно писал пост: bespoyasov.ru/blog/di-ts-in-…

Ещё, кстати, в TypeScript я стараюсь описывать тип-аргументы в дженериках не одной буквой, а нормальными названиями, когда это имеет смысл 😅 Мне нравится руководство по тип-аргументам в C#: docs.microsoft.com/en-us/dotnet/c… (Если в проекте не принято иначе, конечно.)

Я люблю проверять имена всех экспортируемых сущностей на понятность со стороны пользователя. «Если я буду импортировать эту функцию из модуля, я пойму, что она делает? какова область её ответственности? как её использовать?»
Часть треда снова ускакала не туда, простите :–/ twitter.com/jsunderhood/st…

Дальше, чтобы код был понятнее, его должно быть как можно меньше 😃 Всё, что можно может сделать браузер, лучше отдать ему — он сделает это лучше, оптимальнее и быстрее.

Нужно сериализовать форму? Используем FormData: developer.mozilla.org/en-US/docs/Web… Там есть конечно трудности со всякими кастомными контролами, но процентов 80 случаев можно покрыть только ей.

Нужно сделать ленивую загрузку картинок? Испольуем loading="lazy": developer.mozilla.org/en-US/docs/Web… Опять же, полифилим только для тех, у кого это не работает и только если надо 🙂

Ну вы поняли 😃 Кнопки — кнопками, короче.

Непонятное стороннее API лучше прятать за фасадом, чтобы намерение было выразительнее: github.com/kamranahmedse/…

Если язык позволяет, то для выбора из нескольких вариантов используйте pattern matching: - docs.microsoft.com/en-us/dotnet/c… (Это ещё и безопаснее в некоторых случаях.) Для TS тоже есть реализации! - github.com/nrdlab/pattern…

Снова перерыв на работу 🙂 Вечером продолжим!

Продолжим! Ещё один приём, который улучшает читаемость — CQS, command-query separation. - bespoyasov.ru/blog/commands-…

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

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

Ещё один приём рефакторинга, который улучшает читаемость — вынесение деталей в метод или функцию: - bespoyasov.ru/blog/missing-a… Это чем-то похоже на фасад, но над своим кодом: - github.com/kamranahmedse/…
notion image
notion image

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

Ещё одна важная часть читаемого кода — это ошибки и стек-трейс. За сообщениями об ошибках тоже надо следить, потому что это такая же часть кодовой базы. - bespoyasov.ru/blog/make-erro…

Чем чище и понятнее ошибки, тем проще дебажить код ¯_(ツ)_/¯

Кстати, вот мы говорили про вынесение кода в функцию или метод. Можно делать это не руками, а встроенными инструментами рефакторинга в IDE. Например, вот для VS Code инструкция: - code.visualstudio.com/docs/editor/re…

Ну и конечно — документация! В ней нам стоит описывать не «как оно работает», а «почему оно работает именно так». Ответ на вопрос «почему?» — это важный кусок контекста задачи, который может потеряться. Лучше его зафиксировать в документации.

А на вопрос «как оно работает», считаю, должны отвечать тесты 🙂

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

- ...Грамотная архитектура улучшает тестируемость. - TDD также улучшает тестируемость и помогает разбивать легаси на модули. - DI — не обязательно контейнеры, можно проще. - React тормозит на глубоких деревьях 😅

Завтра поговорим о том, как расти в разработке. Обсудим, что лучше: - учиться в университете, - окунуться в боевую разработку, - найти ментора, - читать книги, - участвовать в опен-сорсе.