Правила написания кода: сложные или простые конструкции использовать?

Правила написания кода: сложные или простые конструкции использовать?

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

Правила написания кода: сложные или простые конструкции использовать?

Вводные слова о правилах написания кода

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

int z;
double q,q1,q2;
int k[12] = {12,52,234,56,6,12,31,425,62,123,11,21};
for(z = 0; z< 12; z++)
{
   q= (double)k[z];
   q1 = q*q/1.231;
   q2 = 12*q1 + 0.24 - q*q;
   k[z] = int(q*313 - q2 + q1);
}

Например, когда ранее приведенный код встречается в программе на 30-40 строчек, то это не сильно страшно. Добавив всего пару комментариев, все легко станет на свои места. И это одна из первых вещей, о чем задумывается начинающий программист. Но, как только строчек кода в программе становится далеко за 100-200, такой код становится абсолютно нечитаемым, даже в случае, если вы к каждой строчке будете добавлять комментарии. А уж тем более тестировать и исправлять ошибки в нем становится одним мучением. И это проходил практически каждый.

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

Вы когда-нибудь встречали задачи вида "i++ + ++i"? Обычно, такими конструкциями восхищаются. Дескать это демонстрирует уровень познаний. Однако, на практике такие вещи могут приводить к еще худшим последствиям, чем проблема большого количества бесполезного кода. Все растет от тонкой грани, разделяющей использование простых и сложных конструкций. Не задерживаясь, перейдем к первой проблеме.

Сложные конструкции кода всегда потенциально содержат ошибки 

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

while(...)
{
    counter = (counter++)%2;
    ...
}
 

Казалось бы, какие проблемы могут возникнуть с таким кодом? Даже если вы не понимаете порядок того, как будут применяться операнды, то в режиме отладки можно легко увидеть значения и, собственно, понять "что к чему". Однако, как оказалось на практике, в одном из компиляторов (намеренно название упускается), такой код привел к ошибке. Счетчик каждый раз увеличивался, несмотря на часть "%2". Т.е. значения были 0,1,2,3,4,5... И лишь разделив код на части, счетчик стал нормально высчитываться.

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

Но, это не вся проблема. 

Зависимость сложных конструкций от текущих алгоритмов и компилятора

Представьте, что перед вами записана сложная конструкция вида:

for(int i=0,z=0; z<k && p[z] > 28;z+=2, i=p[z]*i);

Первая часть проблемы вышеприведенного кода. Зависимость от текущих алгоритмов. Если посмотреть внимательно, то две переменные, а именно "k" и "p" задаются не в этой части кода (Не сложно догадаться, что "k" - это вероятно размер массива, а "p" - это массив). Соответственно, весь алгоритм напрямую зависит от внешних переменных. А теперь представьте, что переменная "k" (длина/ограничитель) стала вычисляться по другому, к примеру, увеличилась в два раза или же проверка корректности значения перенесена в другую часть кода. Или же массив "p" не определен и нужно проверять на "null". Тогда такая конструкция начнет разрастаться и на выходе будет получаться следующее:

for(int i=0,z=0; z<k && k > 0 && p !=null && p[z] > 28;z+=2, i=p[z]*i);

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

Вторая часть проблемы кода. Если внимательно посмотреть на условия окончания цикла "z<k && p[z] > 28", то можно заметить одну проблему. В зависимости от того, в каком порядке компилятор будет высчитывать условия (вначале проверку ограничения или проверку длины), на некоторых языках вы можете получить исключение вида "обращение к несуществующему элементу массива". Найти и исключить такого рода ошибки в сложных конструкциях очень не просто. В большинстве случаев, это приводит все к той же задаче расписать код построчно. 

Третья часть проблемы. Плохая читаемость кода. И дело не только в том, что такой код непросто понять. Такой код очень легко пропустить. Вместо положенных по разным стандартам от 2 до 5-6 строчек кода, у вас 1 строчка кода. Если вы обращали когда-нибудь внимание, то циклы гораздо легче заменить, когда они все таки отформатированы и представлены полностью, а не в виде одной брошенной строки среди огромного набора таких же строк.

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

Использование простых конструкций приводит к "захламлению" кода и потери его удобочитаемости

Однако, если вы сделали вывод, что необходимо использовать только простые конструкции, то вы ошибаетесь. Нередко наличие только простых конструкций приводит к еще большим проблемам - код начинает неимоверно разрастаться. Рассмотрим пример на языке JavaScript.

1. Вариант первый - простая конструкция

if (!someObj)
    someObj = {};
someObj.someParam = 'someValue';

2. Вариант второй - усложненная конструкция

(someObj = someObj || {}).someParam = 'someValue';

В зависимости от стиля программирования, разница может занимать от 1-й до 3-х строк. Согласитесь, выделить 2-4 строки на задание переменной кажется не очень хорошей идеей, особенно, если таких объектов будет хотя бы пяток (это от 5 до 15 строк лишнего кода, без учета пустых строк! - этого места вполне достаточно для написания сложного алгоритма преобразования данных). Кроме того, если окажется, что у объекта "someObj" нужно определить свойство для его вложенного объекта, то код вырастет еще больше.

Но, проблема заключается не только в том, что код начинает "захламляться" условными конструкциями и количество строк разрастается "как на дрожжах". Сам код начинает носить разную сложность, что непременно приводит к потери удобочитаемости. Чтобы было легче понять, представьте, что у вас определения переменных и прочие несложные операции начинают занимать основную часть кода (например, 2/3), а важные и сложные части представляют собой лишь мелкие вставки. Другими словами, что алгоритм на 20-30 строк разнесен по 60-100 строкам кода и, соответственно, не помещается на один экран. Тестировать и находить ошибки в таком коде будет очень не просто.

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

Простые конструкции не являются полноценной заменой сложных конструкций

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

1. Вариант первый - простые конструкции и обилие комментариев

// Определили переменную для хранения суммы
double Sum = 0;
// Определили переменную для хранения текущего элемента
double CurrentValue = 1.0;
// Определили коэффициент
double Coeff = 13.0;
// Проходим по циклу 100 раз
for (int i = 0; i < 100; i++)
{
    // Умножаем на 2
    CurrentValue = CurrentValue * 2.0;
    // Делим на коэффициент
    CurrentValue = CurrentValue / Coeff;
    // Возводим в квадрат
    CurrentValue = CurrentValue * CurrentValue;
    // Прибавляем к сумме
    Sum = Sum + CurrentValue;
}

2. Вариант второй - сокращенный вариант и отсутствие комментариев

double Sum = 0, CurrentValue = 1.0, Coeff = 13.0;
for (int i = 0; i < 100; i++) {
    CurrentValue = (CurrentValue * 2.0 / Coeff) * (CurrentValue * 2.0 / Coeff);
    Sum += CurrentValue;
}

Ответьте себе, какой из данных вариантов легче читается и воспринимается (18-строчный простой или 5-строчный усложненный)? Безусловно, однозначного ответа не будет. Для кого-то первый вариант покажется легче, для кого-то второй. Но, в этом и заключается суть. Упростив и разбив код на более простые конструкции, вы не обязательно делаете его лучше и проще. Кому-то после такого разбиения станет легче разобраться в коде, а кому-то наоборот станет сложнее. Другими словами, простые конструкции не являются полноценной заменой сложных конструкций

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

Примечание: В данном случае, проблема разницы точности вычислений не рассматривается.

Так как правильно писать код, используя сложные или простые конструкции? 

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

И самое главное, всегда помните одну важную вещь - "правильного" или "кошерного" кода просто не существует. Есть лишь наборы советов, основанные на определенном опыте.

☕ Понравился обзор? Поделитесь с друзьями!

Добавить комментарий / отзыв

Комментарий - это вежливое и наполненное смыслом сообщение (правила).



* Нажимая на кнопку "Отправить", Вы соглашаетесь с политикой конфиденциальности.
Присоединяйтесь
 

 

Программы (Freeware, OpenSource...)