Как сделать код читаемым
10 способов сделать свой код более читаемым
Перевод статьи Джейсона МакКрири «10 practices for readable code».
Я занимаюсь программированием уже 20 лет. За это время я поработал с 17 командами и в ходе создания сотен проектов программировал на разных языках. Среди этих проектов были и простые сайты-блоги, и APIs с поддержкой 3 тыс. запросов в секунду, и топовые приложения для продаж товаров.
На основе своего опыта, а также прочитанных мною книг, я сумел определить, что в коде имеет самое больше значение: читаемость.
На первый взгляд, читаемость это понятие субъективное. Нечто, зависящее от языка, кодовой базы и команды. Но если копнуть глубже, мы увидим в коде основные элементы, делающие его читаемым.
Многие программисты чересчур близки к компьютерам. Для них важно только то, что код работает, а все остальное не имеет значения. Отсюда распространенные порывы удалить все «человеческие» элементы из нашей работы.
Последние несколько месяцев я потратил на то, чтобы выделить эти элементы в 10 подходов к написанию кода с фокусом на читаемости и уменьшении сложности. Я расписал их детально и приложил отрывки настоящего кода в BaseCode.
К сожалению, многие отбрасывают эти подходы как нечто тривиальное. Слишком базовое. Но я вас уверяю, в каждом кусочке плохого кода, который я встречал, эти подходы не применялись. И наоборот, в каждом хорошем коде можно проследить применение хотя бы одного из этих подходов (если не многих).
Форматирование
На форматирование идет так много энергии. Табы против пробелов. Олман против K&R. Но однажды вы достигнете понимания, что форматирование это не то, что имеет значение в программировании. Примите стандартный формат, примените его к кодовой базе и автоматизируйте. После этого вы сможете перенаправить свою энергию на написание самого кода.
Мертвый код
Все эти закомментированные блоки, неиспользуемые переменные и недостижимый код это гниль. Они прямо говорят читателю: «Мне плевать на этот код». Так и начинается цикл разрушения. Со временем этот мертвый код убьет вашу кодовую базу. Это классический случай теории разбитых окон.
Мертвый код нужно находить и уничтожать. И хотя это не должно быть вашим основным фокусом, руководствуйтесь принципом бойскаутов: «Оставь это в лучшем виде, чем оно было до тебя».
Вложенность кода
Основа практически всего кода это логика. Мы пишем код, чтобы принимать решения, производить итерации и вычисления. Часто это приводит к ветвлению циклов, что создает глубоко вложенные блоки кода. И хотя для компьютера не составляет труда понять этот код, человеку потребуется приложить значительные умственные усилия, чтобы его прочесть.
Распутывайте вложенный код, используя охранные выражения, ранний возврат или аспекты функционального программирования.
Использование объектов
Хотя у нас эра объектно-ориентированного программирования, у нас все еще есть одержимость элементарными типами. Мы обнаруживаем ее в длинных списках параметров, группах данных и пользовательских структурах массивов/словарей. Все это можно преобразовать в объекты. Это не только формализует структуру данных, но и предоставит дом для всей повторяющейся логики, которая сопутствует примитивным типам.
Большие блоки
Поскольку я не очень «дружу» с большими числами, просто скажу, что блоки кода могут достигать критической длины. Когда вы понимаете, что у вас есть большой блок кода, стоит заняться его перегруппированием и перепроектированием.
Этот простой процесс позволит вам определить контекст и уровень абстракции этого блока кода. Благодаря этому вы сможете правильно идентифицировать, что за что отвечает в этом коде и сделать рефакторинг, чтобы получить более читаемый и менее сложный блок.
Имена
Конечно, придумывать имена – сложное дело. Но только потому, что мы делаем его сложным. Есть небольшая уловка, хорошо работающая в отношении разных вещей в программировании, включая именование, – отсрочка. Не тормозите, пытаясь выдумать имя для чего-нибудь. Просто продолжайте программировать. Назовите переменную целым предложением, если нужно. И просто продолжайте писать код. Я гарантирую, что к тому времени, как вы закончите писать этот функционал, наилучшее имя для переменной всплывет в голове само.
Удаление комментариев
Эта простая практика для меня была оригинальной сменой правил игры. Именно это заставило меня сосредоточиться на читаемости.
Несмотря на все мои попытки объяснить этот пункт, всегда найдется кто-то, кто будет меня за это ненавидеть. У этих людей всегда есть на руках именно тот пример, где комментарий был абсолютно необходим.
Конечно, если телеметрическая система телескопа Хаббла должна взаимодействовать с устаревшим адаптером путем возврата 687 для неизвестных показаний, то, вероятно, потребуется коммуникация в виде комментария. Но что касается практически всего остального, вам стоит озадачиться и переписать код таким образом, чтобы он не нуждался в комментариях.
Разумные возвраты значений
Вы должны стараться возвращать более разумные значения. А для действительно исключительных случаев есть лучшие способы коммуникации, чем null.
Правило трех
Подумайте о математической последовательности чисел. Я называю число 2 и спрашиваю: «Какое следующее?» Возможно, это 3 или 4, но может быть и 1, и 2,1. На самом деле вы понятия не имеете. Поэтому я называю еще одно число последовательности – 4 (теперь имеем 2, 4) и спрашиваю: «Какое следующее?» Вероятно, это 6 или 8, или 16. Опять же, несмотря на нашу растущую уверенность, на самом деле мы не знаем. Я выдаю еще одно число из серии, теперь это 2, 4, 16, и спрашиваю: «Какое следующее?» Имея три точки данных, мозг программиста видит последовательность квадратов и определяет, что следующее число – 256. Это правило трех.
Данный пример и без привлечения кода показывает, что мы не должны сразу предполагать абстракцию или дизайн. С помощью отсрочки правило трех противостоит необходимости борьбы с дубликатами. Отсрочка позволяет собрать больше данных для принятия решения на их основе. Говоря словами Сэнди Мец, «дублирование намного дешевле, чем неверная абстракция».
Симметрия
И последний подход. Имеющий отношение к практически поэтической стороне читаемости – симметрии. Из «Implementation Patterns» Кента Бека:
Симметрия в коде это когда одна и та же идея выражается одинаково везде, где бы ни была.
Это легче сказать, чем сделать. Симметрия воплощает творческую сторону программирования. Она лежит в основе многих других подходов: именования, структуры, объектов, шаблонов. Симметрия может варьироваться в зависимости от языка, кодовой базы, команды. Поэтому на достижение симметрии может уйти вся жизнь. Кроме всего прочего, как только вы начинаете стремиться к симметрии в своем коде, в нем появляются более чистые формы и он быстрее принимает свои очертания.
Как сделать код читабельным
Когда-нибудь мы все писали (а некоторые и пишут) плохой код, и, надеюсь, мы все работаем над улучшением наших навыков, а не просто чтением статей вроде этой.
Зачем нам писать хороший код, а не просто производительный код?
Хотя производительность вашего продукта или сайта важна, также важно и то, как выглядит ваш код. Причиной этого является то, что не только машина читает ваш код.
Во-первых, рано или поздно вам придется перечитывать собственный код, и когда это время наступит, только хорошо написанный код поможет вам понять, что вы написали, или выяснить, как это исправить.
Во-вторых, если вы работаете в команде или сотрудничаете с другими разработчиками, то все члены команды будут читать ваш код и пытаться интерпретировать его так, как они понимают. Чтобы сделать это проще для них, важно соблюдать некие правила при названии переменных и функций, ограничивать длину каждой строки и сохранять структуру вашего кода.
Наконец, давайте рассмотрим конкретный пример.
Часть 1: Как определить плохой код?
Самый простой способ определить плохой код, на мой взгляд, — попытаться прочитать код так, как если бы это было предложение или фраза.
Например, взглянем на этот код:
Скриншот плохой версии функции «traverseUpUntil»
Представленная выше функция принимает некий элемент и условную функцию и возвращает ближайший родительский узел, который удовлетворяет условной функции.
Исходя из того, что код должен читаться, как обычный текст, первая строка имеет три грубейших недостатка.
Не совсем очевидно, что возвращается из-за неправильного имени переменной.
Часть 2: Давайте отрефакторим
Скриншот хорошей версии функции «traverseUpUntil»
Затем мы переходим к именам переменных:
Давайте попробуем прочитать: «Присвоить родительский узел родителя родителю, пока у родителя есть родительский узел, а функция условия не возвращает true». Уже гораздо понятнее.
Часто разработчики предпочитают использовать какую-то общую переменную ret (или returnValue ), но это довольно плохая практика. Если вы правильно назовёте свои возвращаемые переменные, становится очевидным то, что возвращается. Однако иногда функции могут быть длинными и сложными, что приводит к большой путанице. В этом случае я бы предложил разделить вашу функцию на несколько функций, и если она всё ещё слишком сложна, то, возможно, добавление комментариев может помочь.
Часть 3: Упрощение кода
Я просто убрал первую строку и заменил «parent» на «node». Таким образом, я пропустил ненужный шаг создания «parent» и перешёл прямо в цикл.
Но что насчёт имени переменной?
Хотя «node» не лучшее описание для этой переменной, оно удовлетворительное. Но давайте не будем останавливаться на этом, давайте переименуем её. Как насчёт «currentNode»?
Так-то лучше! Теперь, когда мы читаем метод, мы знаем, что currentNode всегда представляет собой узел, в котором мы сейчас находимся, вместо того, чтобы быть «каким-то» узлом.
Как сделать код читабельным
Когда-нибудь мы все писали (а некоторые и пишут) плохой код, и, надеюсь, мы все работаем над улучшением наших навыков, а не просто чтением статей вроде этой.
Зачем нам писать хороший код, а не просто производительный код?
Хотя производительность вашего продукта или сайта важна, также важно и то, как выглядит ваш код. Причиной этого является то, что не только машина читает ваш код.
Во-первых, рано или поздно вам придется перечитывать собственный код, и когда это время наступит, только хорошо написанный код поможет вам понять, что вы написали, или выяснить, как это исправить.
Во-вторых, если вы работаете в команде или сотрудничаете с другими разработчиками, то все члены команды будут читать ваш код и пытаться интерпретировать его так, как они понимают. Чтобы сделать это проще для них, важно соблюдать некие правила при названии переменных и функций, ограничивать длину каждой строки и сохранять структуру вашего кода.
Наконец, давайте рассмотрим конкретный пример.
Часть 1: Как определить плохой код?
Самый простой способ определить плохой код, на мой взгляд, — попытаться прочитать код так, как если бы это было предложение или фраза.
Например, взглянем на этот код:
Представленная выше функция принимает некий элемент и условную функцию и возвращает ближайший родительский узел, который удовлетворяет условной функции.
Исходя из того, что код должен читаться, как обычный текст, первая строка имеет три грубейших недостатка:
Не совсем очевидно, что возвращается из-за неправильного имени переменной.
Часть 2: Давайте отрефакторим
Затем мы переходим к именам переменных:
Давайте попробуем прочитать: «Присвоить родительский узел родителя родителю, пока у родителя есть родительский узел, а функция условия не возвращает true». Уже гораздо понятнее.
Часто разработчики предпочитают использовать какую-то общую переменную ret (или returnValue ), но это довольно плохая практика. Если вы правильно назовёте свои возвращаемые переменные, становится очевидным то, что возвращается. Однако иногда функции могут быть длинными и сложными, что приводит к большой путанице. В этом случае я бы предложил разделить вашу функцию на несколько функций, и если она всё ещё слишком сложна, то, возможно, добавление комментариев может помочь.
Часть 3: Упрощение кода
Я просто убрал первую строку и заменил «parent» на «node». Таким образом, я пропустил ненужный шаг создания «parent» и перешёл прямо в цикл.
Но что насчёт имени переменной?
Хотя «node» не лучшее описание для этой переменной, оно удовлетворительное. Но давайте не будем останавливаться на этом, давайте переименуем её. Как насчёт «currentNode»?
Так-то лучше! Теперь, когда мы читаем метод, мы знаем, что currentNode всегда представляет собой узел, в котором мы сейчас находимся, вместо того, чтобы быть «каким-то» узлом.
Как писать читаемый код
Бывает, что посмотрев на старый код, мы говорим: «Его проще переписать, чем поменять». Печально, если речь идет о нашем собственном коде, с такой любовь написанном несколько лет назад. Head of Developer Relations в Evrone Григорий Петров в своем докладе на TechLead Conf 2020 разобрал проблемы, которые приводят к такой ситуации, и рассказал, как бороться с Software complexity problem.
В этой статье пересекаются, казалось бы, непересекаемые вещи: нейрофизиология, проклятие нулевой цены копирования, когнитивная и социальная интуиция. И, конечно же, в ней поднимается тема сложности кода. Вы узнаете о том, откуда она берется, почему ее нельзя убрать и как с ней жить.
В компании Evrone занимаются заказной разработкой сложного софта. Поэтому ее сотрудникам важно писать читаемый код, чтобы клиенты могли поддерживать его сами и благодарить компанию за хорошо сделанную работу.
Но несмотря на двадцатилетний опыт в программировании, Григорий признается: писать читаемый код до сих пор тяжело. И в этой статье мы обсудим все предполагаемые сложности.
Художник рисует мазками
Когда художник рисует картину, он делает это мазками: берет кисть, краски и начинает накладывать по одному мазку. Но нельзя забывать о том, что он всегда может отойти на шаг, чтобы посмотреть на свое творение целиком.
Можно сказать, что программисты тоже пишут код своеобразными «мазками»: идентификатор за идентификатором, оператор за оператором, expression, statement, строчка за строчкой получаются творения в 10, в 100, в 1000 строк кода.
Но, в отличие от художника, программисты не могут «отойти на шаг назад». Художник использует машинерию зрительной коры, у которой есть механизм зума. А когда пишется код, используются механизмы кратковременной и долговременной памяти, у которых механизм зума, к сожалению, архитектурно не предусмотрен.
Поэтому одна из главных проблем, существующих в разработке софта — это то, что мы называем Software complexity problem, что можно перевести на русский как «проблема сложности программ».
Но ведь есть огромное количество других областей, где точно также накапливается сложность. Например, ракетостроение. У людей, которые делают космические корабли, немало сложностей, правда?
Но есть у них и годы обучения, которые начинаются прямо с детского сада. Будущие ракетостроители учат счет, вначале устный, потом письменный, потом приходят в школу, там изучают физику, математику. Проходят годы, они поступают в институт, где узнают, собственно, о строительстве ракет. У них есть устоявшиеся практики о том, как создавать ракеты, а навыки закрепляются повторением. Будущий ракетостроитель проектирует, экспериментирует, совершенствуется, и пятнадцатая по счету ракета таки выйдет за пределы атмосферы.
Проблемы программирования
Нулевая цена копирования;
Если мы уже «построили ракету» — написали софт, у нас нет необходимости писать точно такой же еще раз, чтобы сделать его чуть-чуть лучше. Если разработчик работает над кодом, значит раньше подобного им написано не было. Иначе код был бы скопирован.
Нет понимания «как правильно»;
Индустрия программирования очень молода, мы еще не успели подготовить лучшие практики и не знаем, как «правильно» писать софт. Прямо сейчас можно наблюдать, как монолит объектно-ориентированного программирования, который последние 20 лет был незыблемым, сдает позиции функциональному программированию. Многие топовые разработчики сейчас говорят о том, что наследование — это не очень хороший способ декомпозиции кода. А в языках программирования последнего десятилетия (например, в Rust) в принципе нет классов, как таковых. И они неплохо себя чувствуют.
Отсутствие фундаментального образования;
Из-за молодости индустрии и нулевой цены копирования, в среде программистов отсутствует фундаментальное образование. Есть computer science, но это science. Она про науку, и имеет примерно такое же отношение к прикладной разработке софта как астрономия — к разработке телескопов. Программист, который в университете 6 лет учил алгоритмы и структуры данных, почти ничего не знает про систему управления версиями, идентификаторы, про то, как писать читаемый код и рассказывать этим кодом истории.
Сложно посмотреть, «как делают другие».
Художник может прийти в картинную галерею, посмотреть на разные топовые картины и сказать: «Вот это круто нарисовано. Я сейчас повторю и буду рисовать так же хорошо!». Для этого у него есть интуитивное мышление.
Интуитивное мышление, когнитивные искажения
Наш мозг, конечно, не «чистый лист» с рождения, но и не компьютер с предустановленным софтом. Считается, что мы можем думать ровно те мысли и тем способом, которому обучились за свою жизнь. Интуитивное мышление неплохо справляется на бытовом уровне при оценке диапазонов: оценить насколько красива картина, насколько хорошо сделан ремонт, насколько талантливо выступает артист.
Но если мы попробуем применить интуитивное мышление к чужому коду, наш мозг автоматически выдает результат: «Этот код плохой, ведь его писал не ты. Перепиши все».
У нас нет интуитивного способа оценить «качество кода». Программирование — это принципиально новая область, и наш мозг не может интуитивно применить к нему жизненный опыт из реального мира.
Кроме того, программистам, в отличие от художников, трудно обучаться у мастеров. Мы, конечно, можем прийти в наш аналог картинной галереи — GitHub — и посмотреть там на большие проекты. Но если сделать чекаут проекта с GitHub, там может оказаться полмиллиона строк кода. Это очень много, а у нас нет оптического зума, чтобы просто окинуть код взглядом, не вникая. Поэтому обучаться на примере программистам очень тяжело. Про то, что GitHub это скорее склад строительного материала, а не картинная галерея, я даже говорить не буду.
Так же тяжело заказчикам софта, которым интуиция не помогает понять, что такое технический долг и рефакторинг, и почему команда хочет много денег, чтобы, казалось бы, не сделать ничего особенного.
Так что возвращаясь к вопросу о накоплении сложности, в программировании все то же самое, что и в ракетостроении. Но, из-за отсутствия фундамента, сложность копится намного быстрее, а накопление сложности делает код нечитаемым.
Борьба со сложностью
К сожалению для нас, сложность из кода нельзя убрать. Ведь она — это та польза, которую приносит написанная нами программа.
Но сложность можно перераспределить! Именно об этом пойдет речь в статье.
Гиппокамп — это часть мозга, которая, предположительно, имеет отношение к формированию памяти. Известно, что когда выходит из строя гиппокамп, ломается память.
Как это происходит, не совсем ясно. Но существует такая закономерность, как «Кошелек Миллера»: когда человек смотрит на новые для себя объекты, в среднем, он может удержать в фокусе внимания от 5 до 9 из них.
Современные нейрофизиологи сделали вывод, что Миллер был большим оптимистом, и в реальности число удерживаемых в фокусе объектов ближе к 5. Именно столько новых штук может находиться в кратковременной памяти, прежде чем она начнет давать сбои.
Но у нас есть еще и долговременная память, объемы которой довольно велики. Однако помещение чего-либо в долговременную память занимает немало времени.
Когда человек только учится играть в шахматы, он медленно сканирует шахматную доску, вспоминая правила и пытаясь нащупать некие комбинации. Окно его внимания, содержащее 5 элементов, неспешно ползет по доске.
Но если речь идет об игроке, который сидит за шахматной доской 10-15 лет, то его мозг в автоматическом режиме пользуется привычными паттернами: комбинациями фигур, типичными атаками и защитами.
Когда опытный игрок в шахматы смотрит на доску, новой информации для него очень мало. Именно эта новая информация — то, что он держит в кратковременной памяти, и речь обычно идет всего о 2-3 элементах. Все остальное уже есть в долговременной памяти. Но для такой подготовки требуются годы.
Библиотеки и фреймворки для языков программирования могут стать способом перевести информацию из кратковременной памяти в долговременную.
Если программист много лет пишет на Python и использует requests, он к ним привыкает. Типичные конструкции использования requests — как сделать запрос, как передать и получить JSON, как решать вопросы скорости и задержек — ему привычны. Код, который использует библиотеку, становится для программиста читаемым. В таком коде больше нет сложности. По крайней мере, для этого конкретного программиста.
Если же программист начинает использовать другую библиотеку, читаемость кода для него падает. Поэтому иногда выбор не оптимальной, с точки зрения скоростных или usability характеристик, библиотеки или фреймворка, которые, тем не менее, мега популярны, может быть разумным. Такой код будет намного читаемее для большого количества программистов.
Точно также работает стандарт кодирования, «coding style». Код разработчиков может быть читаемым друг для друга, но только если они хотя бы несколько месяцев поживут с ним. Нейрофизиология утверждает, что несколько месяцев и несколько сотен повторений нужны нашей памяти, чтобы выстроить долговременные связи long-term potentiation, чем бы они ни были.
Все это сейчас очень удобно упаковывается в линтеры. Так что если мы хотим сделать так, чтобы код, который пишут программисты в нашей команде, был читаемым в первую очередь для них самих, мы запаковываем стандарт кодирования в линтеры и настраиваем линтеры в их IDE.
Но память — это долго. Это самый простой, но и самый длительный по времени способ борьбы со сложностью.
Второй по популярности способ — это декомпозиция на части по 5 элементов.
Посмотрим на эволюцию типичной сферической программы в вакууме.
Как правило, она начинается с одного файла, который реализует минимум функциональности. Затем, по мере добавления строк кода, программист интуитивно начинает разделять программу на файлы поменьше. Еще через некоторое время, когда файлов становится несколько десятков, более-менее опытный программист выделяет модули, которые дает язык программирования.
Чуть позже программист начинает использовать абстракции языка. Обычно это классы, миксины, интерфейсы, протоколы, синтаксический сахар. Современные языки программирования, как правило, позволяют программисту бороться со сложностью путем добавления высокоуровневых абстракций и синтаксического сахара.
Через некоторое время, когда абстракции языка программирования исчерпывают себя, и строчек становится несколько десятков тысяч, разработчики начинают с интересом смотреть в сторону DSL: YAML, JSON, Ruby, XML и т.д.
Особенно большой интерес проявляется в Java, где XML-конфиги к программам — просто стандарт де-факто. Но даже если команда не пишет на Java, она с большим удовольствием выкладывает и перераспределяет избыточную сложность в JSON, YAML и в другие места, которые сможет найти.
Наконец, когда строк кода становится очень много, программы начинают делить на модные сейчас микросервисы.
Вспоминается анекдот о том, что любую архитектурную проблему можно решить путем ввода дополнительного слоя абстракции. Кроме проблемы слишком большого количества дополнительных слоев абстракции.
Хорошо, что у нас есть и другие инструменты для того, чтобы писать читаемый код.
Прежде всего метаинформация нужна не компилятору и языку программирования, а людям.
Она как дорожные указатели, которые расставлены по коду. Там, где сложности скопилось слишком много, ее разделяют на части с указанием того, что в этих частях находится. Основная часть при этом одна, все такая же огромная, но внешние «дорожные указатели» позволяют посмотреть на нее под разными углами.
Главный, основной, фундаментальный дорожный указатель — идентификаторы.
Идентификаторы — это переменные, константы, названия функций и классов — все те имена, которые мы даем сущностям в коде.
Недокументированное свойство нашего мозга заключается в том, что часть коры, которая занимается распознаванием слов (зоны Брока и Вернике) очень хорошо умеет их склеивать вместе. Поэтому каким бы длинным ни было слово, с точки зрения нашей рабочей памяти это практически всегда будет одна сущность (в разумных пределах).
竜宮の乙姫の元結の切り外し или Мосгорводоканалстрой — это одна сущность для нашего мозга.
Идентификатор здорово помогает писать читаемый код, если отвечает на вопрос «что это?». Программист пришел в проект, посмотрел на идентификатор, и ему сразу понятно, что. В современных языках программирования для этого есть PascalCase, camelCase, snake_case. Выбирая конкретный стиль, мы выбираем то, что привычнее нашей команде.
В Go все очень тяжело со сложностью, потому что язык практически не предоставляет синтаксического сахара. В книге «Как писать на языке программирования Go» есть параграф про эволюцию идентификаторов. Там написано о том, как бороться с когнитивной сложностью в коде. Выбирая имя для идентификатора, авторы предлагают смотреть на то, что находится рядом с этим идентификатором. Если там что-то очень простое (функция, 2-3 строчки кода, которые очевидны), то идентификатор может быть i или v:
v => users => admin_users
Но по мере увеличения сложности и количества кода, мы хотим увеличивать длину идентификатора, чтобы он лучше отвечал на вопрос «что это?», если такая информация непонятна из контекста.
После идентификаторов идут комментарии, которые отвечают уже на вопрос «зачем это?».
Худший комментарий в коде тот, который пересказывает, что происходит в коде. Но это и так можно увидеть, прочитав код! А вот информация зачем это происходит, как правило, содержится только в голове разработчика.
Топовые мировые программисты нередко пишут код вообще без комментариев. Идентификаторы, которые они используют для переменных, констант, функций, классов, и то, как они разбивают код на части с помощью предоставляемых языком программирования средств, рассказывают историю лучше самых удачных комментариев. Лучшим комментарием является сам код.
Но писать так, как делают это лучшие программисты, тяжело. Поэтому, мы можем добавить в код комментарии, отвечающие на вопрос «зачем?».
Точно также комментарии в коммитах могут давать понимание, зачем сделан этот коммит. А если в таком коммите есть ссылка на тикет, то сложность перераспределяется и туда, давая дополнительные точки опоры при чтении кода через много лет и отвечая на вопрос «зачем это было сделано?».
Документацию же можно рассматривать в качестве последнего бастиона. Если не удалось сделать код, который отвечает на вопрос «зачем?», не получилось добавить в него комментарии, которые отвечают на этот вопрос, и с комментариями в коммитах и тикетах тоже не сложилось, открываем readme.md и пишем там большой архитектурный абзац.
У документации есть огромные риски рассинхронизироваться с кодом, поэтому, когда мы пишем читаемый код, нужно постараться выносить что-то в документацию только в том случае, если выбора нет. Например, когда у нас очень большой проект.
Автогенерация документации — это отдельная история. Многие примеры хорошего кода, которые мы видим: фреймворки и библиотеки. При их изготовлении нам важна документация, поэтому мы документируем каждый метод, а потом автогенерим документацию. Но это нужно делать с умом.
В роли документации могут выступать и тесты. Поскольку они показывают пути выполнения.
В последние 5-10 лет в динамические языки программирования пришли типы. Они выполняют роль своеобразных «капканов» для ошибок. Когда программист пишет код, он может воспользоваться Gradual подходом современных языков. И там, где сложность повышена, добавить типы, чтобы в коде расставились несколько «капканов».
Если программист через какое-то время (например, спустя полгода) воспользуется этим кодом неправильно, «капкан» сработает, подчеркнет ему строчку в IDE красным, и все сразу станет понятно.
Gradual подход к перераспределению сложности
Gradual подход к написанию читаемого кода по больше части вращается вокруг цифры 5.
Есть несколько способов перераспределения сложности:
Gradual decomposition;
Если в нашем коде собралось больше пяти элементов, мы пробуем распределить их декомпозицией по файлам, по модулям, по классам, по функциям: в зависимости от того, что у нас есть в языке программирования.
Gradual meta information;
Если мы понимаем, что у нас уже есть распределение на множество частей, начинаем добавлять метаинформацию: давать описательные имена идентификаторам, чтобы они отвечали на вопросы «что это?» и «зачем это?».
Gradual typing.
Наконец, когда сложность продолжает скапливаться, мы добавляем типы, как «капканы» на будущее. Чтобы по возвращению к этому коду через какое-то время, «капканы» сработали и защитили нас.
Gradual подход работы со сложностью можно сформулировать в одном предложении: если количество новых вещей в коде намного превышает цифру 5, нужно использовать один из способов перераспределения сложности из списка выше.
Вопрос о том, что такое «новая вещь», остается немного за кадром. Это зависит от бэкграунда разработчика: сколько лет он пишет код, какие языки программирования, фреймворки, подходы знает.
Если в команде есть разработчики разного уровня (например джуниоры и сеньоры), они не смогут писать код, который будет читаем друг для друга. То, что не является новинкой для сеньора, который 20 лет пишет код, для джуниора ею будет. Поэтому код, который напишет сеньор, будет очень простой, понятный, хорошо читаемый — но для сеньоров. А для джуниоров количество «нового» и, соответственно, сложности в таком коде будет зашкаливать.
Практика показывает: если мы хотим, чтобы код, который пишут наши разработчики, был читаемый в первую очередь для них самих, квалификация тех, кто занимается этим в одной команде, должна быть примерно одинакова.
Писать читаемый код сложно. И Gradual поход, о котором шла речь в статье, не всегда применим. Разработка софта очень разная: есть разработка микроконтроллеров, есть разработка игр, есть бизнес-автоматизация по спецификациям, и там правила игры совершенно другие.
Но в большинстве случаев Gradual подход, который крутится вокруг цифры 5, является неплохой стартовой точкой.
Конференция, полностью посвященная инженерным процессам и практикам, TechLead Conf 2021 пройдет 12 и 13 апреля. Билеты можно приобрести здесь. Вы можете успеть купить их до повышения цены!
А пока мы все ждем апреля, приглашаем вас на Quality Assurance Webinar. На нем поговорим о пирамиде тестирования, узнаем, как найти UI тесты, которые легко могут быть перенесены на более низкие уровни, и разберемся в инфраструктуре тестирования в браузерах.
Мероприятие начнется 21 января в 18:00 мск. До встречи!