Количественная оценка понятности кода

image

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

Но давайте посмотрим на проблему с другой стороны. Что мы делаем, когда разбираемся с чьим-то кодом? Как происходит сам процесс изучения кода? Мы листаем функции, ищем определения переменных, классов, переходим от функции к функции, от файла к файлу.


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

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

Вот несколько примеров навскидку, которые помогут понять суть этой метрики:
Пример 1: вы встречаете в коде переменную с ни о чем не говорящим названием (например, j). Вы найдете определение этой переменной (совершите переход), и, если повезет, там окажется комментарий, поясняющий, что она означает. Если не повезет, и комментария не окажется, то вам придется искать все места, где она используется (совершать переходы) и пытаться понять ее назначение. А если даже комментарий к переменной и был, то вы, вероятно, скоро забудете его, и придется снова к нему возвращаться (совершать переход).

Пример 2: в коде вы встретили вызов функции с ни о чем не говорящим названием. Более того, не понятно, какой результат может выдавать функция. Вам придется совершать переход к реализации этой функции, несколько раз перемотать ее вниз и вверх, чтобы понять, что она делает, и какой результат возвращает. А если функция окажется длиной строк в 200-300, то листать придется много.

Пример 3: в анализируемой функции присутствует множественная вложенность: условия в циклы, циклы в условия. Держать в памяти это очень сложно, и вы будете периодически пролистывать код вверх, чтобы удерживать весь контекст в памяти.

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

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

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

Предвижу вопрос, что такой способ оценки понятности не лишен субъективности. Соглашусь. Я вижу здесь две основные сложности.
Во-первых, кто-то привык часто переходить по коду, а кто-то способен держать контекст в памяти. Но все же этот способ более объективен, чем просто оценка, основанная на мнении. А, учитывая простоту реализации такой оценки, интересно было бы посмотреть результат.
А во-вторых, поначалу разработчики могут намеренно меньше (больше) листать код, чтобы получить желаемую оценку. Но когда такая проверка станет привычной практикой в организации, то, вероятно, подтасовки исчезнут.

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

Реализовано это может быть в виде плагина для IDE.
Share post

Similar posts

Comments 34

    +1
    Для руби уже есть: codeclimate.com/

    Пользуюсь и нарадоваться не могу :)
      +2
      Спасибо за ссылку! Прогнал Rails-проект, получил интересные результаты :)
      Когда мы эту статью обсуждали с друзьями на работе, родилась идея для автоматической оценки читабельности имен переменных и функций: проверять их на соответствие английским словам, переменные должны быть существительными и быть больше некоторой минимальной длины, функции состоять из связки английских глагола-существительного. В случае несоответствия слова можно подчеркивать красным, как в Word'е.
        +1
        Это уже перебор, я считаю :)
          0
          Это не решает проблемы с тем, что названия переменных могут не следовать соглашениям, смыслу содержимого или контексту. Пример из жизни на руби:

          partner = PartnershipProfile.find(profile_id)
          if partner && @order.available_for_partner?(partner) # на самом деле в качестве аргумента для available_for_partner? подразумевался partner.user
            @order.mark_with_prid partner.id
          end
          


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

          Надеюсь не притянуто за уши*
            0
            Чтобы отследить такие вещи, анализатор должен понимать смысл написанного. Поэтому оставим эту задачу на откуп человеку. По крайней мере пока.
            А чтобы помочь будущим разработчикам в анализе кода, можно заранее определиться с терминологией, на подобии того, как заранее продумываются интерфейсы. А то я часто встречал примеры (иногда и сам грешил), когда одна вещь в разных местах называется по-разному, разные вещи называются одинаково, а некоторые названия придумываются мимоходом в процессе кодирования. Мне вообще кажется, что задачи проектирования и реализации должны разграничиваться. Сначала придумываем, что и как хотим сделать, а потом делаем. Если пытаться проектировать «по ходу», получается плохо. Проверял.
        +2
        Очень компактный код может быть очень сложным для чтения
        float InvSqrt (float x){
            float xhalf = 0.5f*x;
            int i = *(int*)&x;
            i = 0x5f3759df - (i>>1);
            x = *(float*)&i;
            x = x*(1.5f - xhalf*x*x);
            return x;
        }
        

        Поэтому, в метрику нужно еще включить время, потраченное на чтение кода. И почему бы не использовать метрику основанную исключительно на времени потраченном на его понимание?
          0
          Я думал об этом. Но способ со временем подойдет только для небольших фрагментов кода, так как при анализе большого объема кода человек может отвлечься и забыть поставить таймер на паузу.
            0
            Напрасно название оставили. Было бы интересно поугадывать. Других примеров нет?
              0
              Уж очень маловероятно что кто-то догадался бы. Тут либо графики строить, либо знать, либо сотни лет сидеть курить что это такое).
              Нет, и это с трудом нашел.
                +2
                Вот мне и было интересно, сколько времени потребуется. Думаю, 5 минут хватило бы: если догадаться, что строчка x = x*(1.5f — xhalf*x*x); это уточняющая итерация, то получаем x=(3*x-a*x^3)/2, т.е. a*x^3=x, x=1/sqrt(a). Для контроля можно проверить, так ли выглядит метод Ньютона для этой функции. А потом еще и вычислить магическую константу и сравнить с приведенной в коде. Но для понимания, как это работает — всё это не нужно, и так очевидно. Вот если бы пришлось разбираться, почему не работает, или оценивать точность — надо было бы пройти каждый шаг, с графиками.
              0
              Ну к счастью такие функции как в примере либо хорошо известны(как ваш пример), либо тщательно задокументированы. Поэтому читать обычно нужно псевдокод.
                +2
                Не помню, чтобы я раньше встречал эту функцию. Она «хорошо известна» в качестве чего? Примера эффективного кода? Примера нечитаемого кода? Часть стандартного курса по С? Или часть программистского фольклора?
                И неужели бывают люди, способные придумать и реализовать такой метод, а потом его «тщательно задокументировать»?
                  0
                  Почитал. Смесь эффективного кода и фольклора. И действительно, очень тщательно задокументирована :)
                    0
                    Она хорошо известна как пример быстрого на ходу придуманного очень быстрого алгоритма решающего задачу вычисления вычисления обратного квадратного корня с достаточно высокой точностью. ЕМНИП это был очень серьёзный прорыв в 3д графике лет 10 или 15 назад. Даже на хабре была статья именно про эту функцию.

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

                    В данном случае автор алгоритма скорее всего мог просто описать суть алгоритма и дать отсылку на документ с его полным мат. обоснование.

                    А когда я говорил тщательно задокументированы, я имел в виду что вы открываете например
                    en.wikipedia.org/wiki/Fast_inverse_square_root
                    И всё там читаете.
                      0
                      Так я её открыл и прочитал. Секцию overview of the code " с оригинальными комментариями" — но это явно комментарии не автора, а одного из читателей. Остальная часть статьи, насколько я понял, последующие исследования этого артефакта (интересно, кстати, есть ли такие, кому интереснее их читать, чем разобраться самому — не могу представить себе их мотивы). Проблема с авторскими комментариями могла бы оказаться в том, что автору, придумавшему «ультраоптимизированный и непростой алгоритм» может не хватить воображения, чтобы понять, насколько непонятливыми могут быть читатели его кода. Он находится на два-три уровня выше их. Кроме того, на комментирование может не оказаться времени.
                      Интересно, как можно было «на ходу» подобрать константу, да так, что последователи не смогли её улучшить.
                        0
                        Насчет «не смогли улучшить» я был неправ: константа 0x5f375a85 даёт лучший результат (хотя Chris Lomont в своей статье считает, что правильное значение 0x5f375a86).
                0
                Лучшее враг хорошего, не переборщите в погоне за идеально читаемым текстом кодом.
                  0
                  Чтобы не переборщить, надо знать, когда остановиться. А для этого нужна метрика :)
                  +1
                  Т.е. выходит, что самая идеальная программа — это один файл с одной функцией? Или я чего то не понял?
                    0
                    Нет, конечно! Если программа большая, то какого же размера будет эта функция, и сколько в ней будет переменных! Листать и переходить в ней придется много, из-за чего метрика уменьшится.
                    Говоря про главную функцию я имел ввиду, что если она понятно написана, то не обязательно нужно будет смотреть вызываемые функции. Из вызова (названия и параметров) будет понятно их назначение.
                      +1
                      На мой взгляд, правильнее считать зависимости в коде. Думаю, именно увеличение зависимостей между компонентами затрудняет понимание кода больше всего.
                        +1
                        Я не совсем это имел ввиду. Зависимости — это другая грань качества кода. Предложенная же метрика ориентированна именно на понятность для человека. Хотя, скорее всего, код с небольшим числом зависимостей будет и достаточно понятным
                    +1
                    Хм, я предлагаю такой более краткий вариант — все функции должны быт «чистыми» и все данные должны быть иммутабельны? Это возможно позволит убрать все пункты? :)

                    Кстати, если бы вам пришлось оценить с точки зрения ваших пунктов вот такой код
                    let mapNeighbors l = zip3 (Nothing : map Just l) l (map Just $ tail l ++ [Nothing])

                    Он бы прошел пункты, например третий? Ну и первый со вторым :)
                      0
                      Интересно какие результаты будут для С-кода из GNU libc к примеру
                        +7
                        А как же классическая единица измерения (не)понятности кода — WTF/час?
                          +5
                          image
                            +1
                            Это сделало мой день. Спасибо!
                          +2
                          Вспомнилось добротное: «Качество кода определяется количество Чезанахов».
                            0
                            Примеры 1 и 2 показывают главную слабость попыток автоматически получать метрики понятности кода. Что такое «ни о чем не говорящее название» машина определить не может. Например, j в одном контексте не будет ни о чём говорить, а то и будет вводить в заблуждение человека, привыкшего к «типовому» использовании этого имени как второго индекса, а в другом (например, численном взятии двумерного интеграла или вычислении суммы матриц) будет вполне понятна (для владеющего предметной областью).

                            Ну и количество строк (как и уровень вложенности) само по себе мало что говорит о понятности. Если считать, то считать количество инструкций (а не строк), а также их сложность.

                            Плюс надо как-то учитывать «инлайновость», делает ли сворачивание тела, например, цикла в отдельную функцию код понятнее, или наоборот ещё больше запутанней. Что понятней:
                            for(sum = 0, i = 0; i < max_x, i++) {
                              for(j = 0; j < max_y, j++) {
                                sum += a[i][j];
                              }
                            }
                            

                            или
                            for(sum = 0, i = 0; i < max_x, i++) {
                              sum += array_sum(a[i], max_y);
                            }
                            
                            array_sum(a, len)
                            {
                              for(sum = 0; i = 0; i < len, i++) {
                                sum += a[i];
                              }
                              return sum;
                            }
                            
                            

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

                              0
                              Извините, я не понял, что вы имеете ввиду. По-моему, все приведенные вами примеры хорошо укладываются в предложенную метрику. Если бы вместо суммирования был какой-то сложный код, то его стоило бы вынести в функцию с понятным названием, и тогда можно было бы в ее реализацию не заглядывать.
                              По поводу сложности инструкций — я понял вашу мысль. Выше был комментарий, и я с ним согласен, что для небольших фрагментов кода можно считать не скроллы и переходы, а время потраченное на анализ кода.
                                0
                                Это два взаимоисключающих примера. В первом случае вложенность два, во втором — один. По идее второй лучше должен быть.
                                  0
                                  Да, согласен. А в сложных функциях, где вложенность неизбежна, она может компенсироваться функцией с понятным названием.
                              0
                              Без определения степени соответствия имен сущностей их назначению — такая метрика не будет работать.

                              Потому что меня не устраивает метрика, в которой данные функции имеют одинаковый вес:
                              int make_string() { return 1;}
                              string make_string() {return "";}
                              string ghiskjrfd() {return "";}
                              
                                0
                                Кстати, дядя Боб так и говорит: «Код должен читать сверху вниз, причём, читающий должен иметь возможность в любой момент прекратить чтение также как при чтении газеты, т.е., читающий видит заголовки, краткое введение и проходит вглубь, если сам того захочет».

                                Интересно, что Кристин Горман выступила с критикой метода дяди Боба, который он назвал «Extract till you drop» (благодаря которому возникает код по которому можно идти сверху вниз, останавливаясь там, где ты хочешь, не углубляясь в ненужные детали). Однако в блоге Роя Ошероува я обнаружил запись, которая отвечает на её критику.

                                Выступление Кристины Горман.
                                Разъяснение смысла от Боба Мартина

                                Only users with full accounts can post comments. Log in, please.