Введение

Программисты постоянно стараются сделать код лучше, используя для этого различные практики. Однако само понятие хорошего кода крайне расплывчато, о чём свидетельствует одно только количество книг, посвящённых этой теме, а также их объём. Например, книга "Чистый код'' Р. Мартина содержит почти 500 страниц. Неужели нет возможности выразить хотя бы основные критерии хорошего кода короче?

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

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

Кроме того, для мышления необходимы память и время. Любая модель мышления (модель вычислений) обязана включать эти два понятия. Действительно, в машине Тьюринга роль памяти играет лента, а роль времени – головка. Аналогично, в современном компьютере эти две роли выполняют RAM и CPU, соответственно.

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

Качество

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

Любой программе или абстрактному объекту можно поставить в соответствие дерево, где корень – сама программа, а дочерние вершинами являются её элементы, далее элементы элементов, и т.д. (рекурсивный процесс).

Каким способом можно измерить подобное дерево? В наличии два способа.

Первый, можно пересчитать количество дочерних вершин в дереве, или другими словами, рекурсивно посчитать количество поддеревьев. Число таких всех поддеревьев будем называть объёмом и обозначать как \mathcal{V}.

Несколько более осмысленным измерением будет рекурсивный подсчёт только уникальных поддеревьев, их число будем называть сложностью и обозначать как \mathcal{C}.

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

С учётом вышесказанного, можно определить качество \mathcal{Q}(x) программы x как отношение её сложности к объёму:

 \mathcal{Q}(x) = \frac{\mathcal{C}(x)}{\mathcal{V}(x)}

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

Гармония

Рассмотрим, как выглядит оптимальная программа, отталкиваясь от того, как работает аппарат балансировки в принципе (это одна из важнейших функций мышления). При рассмотрении более конкретных программ могут появляться более сложные критерии оптимальности, но есть ли понятие оптимальной программы как таковой?

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

Уравнение балансировки чисел выглядит так:

\frac{x y}{(x + y) (x - y)} =1

Из этого уравнения выводится значение золотого сечения \Phi\approx 1.618.

Теперь представим, что x и y – это компоненты программы. Мы не можем напрямую применить данное уравнение к программам, т.к. их нельзя умножать друг на друга, а также складывать и вычитать. Тем не менее, мы можем провести преобразования, которые сохранят смысл уравнения и сделают его пригодным для обобщённых структур и в частности, программ.

Если опустить длинное математическое обоснование, то для программ (и абстрактных объектов вообще) уравнение балансировки приобретает вид:

\frac{\mathcal{C} (x)\mathcal{C} (y)}{\mathcal{C} (\mathcal{L} (x,y)) \mathcal{C} (\mathcal{O} (x,y))} = 1

Здесь \mathcal{L}(x, y) и \mathcal{O}(x, y) – это функции сходства и различия. Данные понятия являются неотъемлемой частью модели мышления. Действительно, о каких бы двух объектах ни шла речь, мы всегда можем указать (спросить), что между ними общего и в чём они различаются.

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

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

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

Левую часть уравнения обозначим как \mathcal{H}(x,y) – значение гармонии x и y(для большего количества элементов гармония считается по всем парам). Хотя точный подсчёт \mathcal{H} для кода на высокоуровневом языка пока затруднителен, похожесть и непохожесть (например, избыток похожих компонентов) глаз программиста распознаёт быстро и безошибочно, "на уровне BIOS'а''.

Таким образом, оптимальный код должен иметь значение \mathcal{H} ближе к единице.

Целостность

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

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

Приведём пример. Следующий код (на языке C#) отражает поведение абстрактного (математического) времени. Чтение, изменение, запись – это основные функции времени, которые образуют любую модель вычислений.

    public sealed class marker {
        public dynamic argument;
        void move(dynamic previous, dynamic next) {

            argument = previous?.read();
            argument = transform(argument);
            next?.write(argument); }}

На первый взгляд, с этим кодом всё в порядке. Однако задумаемся вот о чём. Входное множество (dynamic previous, dynamic next) состоит из двух элементов. Если мы передадим в функцию move один и тот же объект, по факту это приведёт к тому, что множество будет состоять из одного элемента (одинаковые элементы в множествах не допускаются). Контракт, который заключается в неизменности объекта previous, будет нарушен.

Подобное нарушение контракта распознаётся в математике как противоречие, а в программировании как исключение, поэтому (в общем случае) необходима дополнительная строчка:

            argument = previous?.read();
            argument = transform(argument);
            if(previous?.Equals(next)) throw new Exception();
            next?.write(argument); }}

В более общем смысле, можно показать, что противоречие возникает в случае, когда уничтожается или игнорируется какой-либо способ отличить одно от другого. В указанном примере, когда previous равен next, утрачивается способ отличить предыдущий объект от следующего.

Если код написан с учётом этого аспекта, т.е. не допускает потерю знаний, это эквивалентно его строгости. Понятно, что поведение строгого кода более предсказуемо, нежели не строгого. Долю строгого кода в приложении будем называть целостностью и обозначать \mathcal{W}.

Интегральная характеристика

Мы показали, что исходя из аспектов программирования – рассуждения и балансировки, а также из необходимости сохранения знаний и следования контракту, предпочтительными для кода являются:

  • бо́льшее качество \mathcal{Q};

  • лучшая гармония, т.е. меньшее расстояние между \mathcal{H} и единицей;

  • бо́льшая целостность \mathcal{W}

Объединим, для программы x, эти условия в интегральную характеристику \mathscr{P}(x):

\mathscr{P}(x)=\frac{\mathcal{Q}(x)}{|\;\mathcal{H}(x)-1\;|}\mathcal{W}(x)

Таким образом, хорошая программа будет отличаться более высоким значением характеристики \mathscr{P}. На практике подсчитать эту величину для программы на языке высокого уровня пока затруднительно. Тем не менее, программист достаточно легко оценивает качество и гармонию (сбалансированность) программы, просто глядя на код. Что касается целостности, она также может быть оценена при анализе кода.

Надо указать, что сложность программы естественным образом должна быть ограничена сложностью решаемой задачи. Повышение качества должно достигаться за счёт снижения объёма, а не увеличения сложности, в противном случае слепое следование формуле \mathscr{P} может привести к негативным результатам.

Например, если в программе нужно сложить два числа, то можно искусственно увеличить сложность, написав целый калькулятор и применив его для сложения. Результирующий код даже может обладать лучшим значением \mathscr{P}, что противоречит здравому смыслу.

Введём понятие полной сложности решаемой задачи \mathcal{C}_T. Понятно, что если задача решена целиком, то сложность программы не может быть меньше полной сложности: \mathcal{C}(x) \ge \mathcal{C}_T. Однако дальнейший рост сложности не оправдан, поэтому формулу \mathscr{P}необходимо переписать:

\mathscr{P}(x)=\frac{\mathcal{C}_T\mathcal{W}(x)}{\mathcal{V}(x)|\;\mathcal{H}(x)-1\;|}

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

Отметим, что значения \mathcal{C}, \mathcal{V}, \mathcal{H} и \mathcal{W} не являются изолированными. Улучшение одного параметра может иметь (и, как правило, имеет) цену ухудшения другого. По этой причине, элементы формулы не должны применяться поодиночке.

Таким образом, хорошим должен считаться код, который максимизирует значение интегрального критерия, даже несмотря на то, что значения объёма, гармонии и целостности (возможно) не достигают своих максимальных значений по отдельности. Можно показать, что следование принципам проектирования приводит (в общем случае) к максимизации интегрального критерия \mathscr{P}(x).

Размеры

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

Аналогично, разум человека значительно хуже воспринимает длинный код, а также код с большим количеством длинных строчек ("простыня''). По опыту, можно указать, что оптимальная длина класса – до 250 строк, метода – до 30-40 строк, списка параметров – до трёх, в редких случаях до 5 параметров. Излишне длинный код также говорит о том, что он недостаточно насыщен структурой.

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

Заключение

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

Комментарии (9)


  1. anzay911
    22.03.2022 09:40

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

    Программистская шутка: а делить-то можем!


  1. kovserg
    22.03.2022 13:15
    +5

    Целью статьи является, при помощи математики, превращение данного знания из эмпирического в строгое и теоретически обоснованное

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


  1. samsergey
    22.03.2022 16:05
    +1

    Прошу вас, поясните смысл и природу уравнения балансировки чисел. Что оно позволяет узнать о числах x и y, кроме того, что они пропорциональны с коэффициентом Ф?


    1. Ramayasket Автор
      23.03.2022 11:04
      -1

      Если есть два числа x и y, то у них могут быть две крайности. Первая, это x=y, а вторая x=0 или y=0. Соответственно, уравнение балансировки чисел показывает, какое отношение x и y является оптимальным (Ф). Балансировка – это поиск оптимального решения между двумя крайностями, и она работает не только с числами, как и показано в статье.


  1. blood_develop
    22.03.2022 19:30
    +1

    Ждем-с практического обоснования


  1. Shadasviar
    23.03.2022 07:05
    -1

    Интересная статья, возможно лет через 20 на основе таких вот формализаций будет работать автоматическая проверка кода на качество в каждой иде.


    1. Ramayasket Автор
      23.03.2022 11:04
      -1

      20 лет это перебор! Лет 5 самое много, работа-то не остановилась!


    1. Vasiliy_S
      23.03.2022 13:18

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

      Останется еще найти способ машинам самостоятельно генерировать себе задачи.


      1. TNTima
        23.03.2022 14:06

        Минимизация рассеивания энергии (энтропии)?

        Изучение вселенной?