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

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

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

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

Итак, вот они ...

Организуйте свой проект для людей

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

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

Все разработчики сразу же видят гораздо лучший вариант. Черт возьми, почему бы даже не разработчикам легко понять, что он делает? Есть отличный подход под названием кричащая архитектура, предложенный дядей Бобом. Короче говоря, вы должны структурировать свой проект так, чтобы он сразу кричал о том, что он делает, а не о том, в какую структуру он встроен.

Оставайся последовательным

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

Ваш стиль кода все еще может развиваться и улучшаться с течением времени (и это, безусловно, должно), но убедитесь, что остальная часть команды принимает участие!

Умеренно используйте условные обозначения

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

Например, одно из таких соглашений будет использовать lowerCamelCase для всех переменных и методов. Это уже вторая натура для многих программистов. Его применение можно автоматизировать, оно простое и чрезвычайно популярное, поэтому вам не нужно тратить время на его объяснение своим коллегам. Они быстро поймут это, просто взглянув практически на любую часть вашего кода.

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

Возьмем, к примеру, Антипаттерн Impl. У вас есть интерфейс с осмысленным именем, и, поскольку ваш класс реализации не может иметь одно и то же имя, вы просто исправляете его суффиксом Impl. Итак, теперь вы должны уведомить всех в команде, что они должны сделать это для единообразия, а также всегда сообщать об этом каждому новому члену. Тогда вам всем нужно быть особенно осторожными при проверке кода, чтобы убедиться, что это соглашение было применено. И стал ли в результате ваш код проще? Нет. Это безопаснее? Нет. Это более читабельно? Нет. А также, что произойдет, если вам понадобится две или более реализаций? Impl2? Вы можете сказать, что все это, по крайней мере, согласованно, но согласованность здесь обходится слишком дорого.

Не сокращать

Сокращения вводят догадки и даже двусмысленность. Предположим, у вас есть переменная с именем stringCln. Cln означает «сбор»? Или «колонна»? Или «клонировать»? Все они могут иметь правильное применение в программе, и вам нужно изучить окружающий код, чтобы понять, что означает аббревиатура. Это заставляет вас оглядываться и гадать, вместо того, чтобы на мгновение понять, было ли использовано полное слово. Если случаев такого именования много, усилия, необходимые для чтения кода, значительно возрастают.

Избегайте имен, которые стимулируют спаривание

Возможно, вы видели много SomethingManager, BaseDoohickey или других подобных классов за свою карьеру. Подумайте на мгновение, что такое Manager или Base. Термин «менеджер» говорит людям, что это класс, которому принадлежит все, относящееся к остальной части имени класса. Термин «базовый» обычно используется в наследовании и, как и «менеджер», побуждает других просто помещать туда много различного кода, чтобы сделать его легко доступным для нескольких дочерних элементов.

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

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

Например, вместо того, чтобы иметь один большой MessagingManager, разделите эту функциональность на несколько классов, таких как MessageReceiver, MessageSender, MessageComposer, MessageForwarder, ContactFinder, AttachmentUploader и т. Д. Обратите внимание, как каждое из этих имен делает назначение своего класса намного яснее, чем Manager. Кроме того, наличие большого количества небольших классов никогда не является проблемой (при условии, что они хорошо организованы). Практически всегда иметь большой, выполняющий много дел.

Всегда указывайте единицы измерения

Представьте, что вы столкнулись с методом с именем getTimestamp(). Выражена ли временная метка в секундах? Это в миллисекундах? Предполагая, что вывод может привести к ужасным проблемам. Если бы этот метод назывался getTimestampSeconds(), вам не нужно было бы изучать документацию или открывать тело метода для исследования. Кроме того, что, если тело недоступно (например, библиотека с закрытым исходным кодом) и никто не потрудился написать документацию? Это случается слишком часто.

Держите свои методы (и классы) маленькими и не запутанными

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

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

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

Будьте декларативными и отодвиньте детали реализации назад

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

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

// The non-declarative version 
void f(char **s, int l) {
    int i;
    
    for (i = 0; i < l; i++) {
        char *c = s[i];
        
        while (*c != '\0') {
            if (*c >= 'a' && *c <= 'z') {
                *c = *c - ('a' - 'A');
            }
            
            c++;
        }
    }
}

а также

// The declarative version (with omitted helper functions) 
void f(char **s, int l) {
    forEachString(s, l, &toUpperCase);
}

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

Какое из двух тел требует меньше времени, чтобы понять? Даже если вы не знаете C или что-либо еще о указателях на функции, вы все равно должны понимать вторую реализацию. И если вы знаете C, вы, вероятно, поймете вторую функцию раньше первой. Если все обстоит наоборот, вы, вероятно, могли бы заработать немного денег в качестве подопытного в некоторых областях исследований :)

Теперь давайте посмотрим на функцию forEachString(...):

void forEachString(char **s, int l, void (*modify)(char *)) {
    int i;
    
    for (i = 0; i < l; i++) {
        char *c = s[i];
        modify(c);
    }
}

Обратите внимание, как объявление i, цикл и его первая внутренняя строка были просто скопированы из первого примера f(...) без каких-либо изменений. Это все детали реализации, которые нам нужны на данный момент, поэтому мы обобщили и извлекли модификацию строки во внешнюю функцию, на которую ссылается modify.

Наконец, мы пишем строку с заглавной буквы toUpperCase(...):

void toUpperCase(char *s) {
    char *c = s;
    
    while (*c != '\0') {
        if (*c >= 'a' && *c <= 'z') {
            *c = *c - ('a' - 'A');
        }
        
        c++;
    }
}

Здесь вы можете заметить, что большая часть тела первой f(...) функции только что была перемещена в toUpperCase(...) - последнюю вызванную «вспомогательную» функцию. Таким образом, даже без изменения неясных имен функций, параметров и переменных, нам удалось сделать исходную функцию более удобочитаемой, просто заменив ее части декларативными вызовами функций.

Отключить автоматически сгенерированные комментарии

Многие IDE добавят что-то подобное при создании нового файла класса:

/** 
  * Created by someuser on 1/1/2018
  *   
  * Insert class description here
  */

Это просто бесполезно и шумно. Контроль версий может рассказать вам больше о том, кто создал или изменил класс, чем комментарий. Я также часто видел, как текст по умолчанию, подобный приведенной выше строке Insert class..., остается в коде через месяцы или даже годы после создания класса.

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

Жертвуйте удобочитаемостью ради производительности, только если это серьезно влияет на ваших пользователей.

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

Конечно, пользовательский опыт имеет ключевое значение. Так что, если есть существенная проблема с производительностью и ее исправление требует жертв для удобочитаемости - сделайте это! Тем не менее, пожалуйста, сделайте это как можно более ответственно.

Будьте открытыми и уязвимыми

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

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

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

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

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