Основные принципы чистого кода
Прежде чем говорить о базовых правилах оформления кода программы, давайте определимся с тем, какой код мы будем называть "чистым".
Чистый код — это код, написанный и оформленный так, что его легко читать, понимать, изменять и поддерживать. Причём не только вам, как автору кода, но и другим программистам.
Важно!
Всегда помните, что в первую очередь код должен быть понятен другим программистам.
Любой дурак сможет написать код, который поймет машина. Хорошие программисты пишут код, который сможет понять человек.
(c) Martin Fowler
Если читая код вы постоянно возвращаетесь на предыдущие строки с вопросами навроде "Что лежит в этой переменной?", "А что должна делать эта функция?" или даже "Что я здесь чёрт побери написал?", то скорее всего вы имеете дело с кодом, который не назовёшь чистым.
Чтобы наглядно продемонстрировать разницу, давайте рассмотрим следующий пример.
Пример
Написать программу, которая определяет, является ли введённое число простым или составным.
На вход программе можно подавать любое натуральное число, большее 1.
Если число простое, то программа должна выводить строкуprime, иначе — строкуcomposite.
Листинг 1. Пример кода без оформления
#include <stdio.h>
int main(void){
int N;
int tmp =1;
scanf("%d",&N);
for(int i=2;i<N;i++){
if(N%i==0)tmp=0;}
if(tmp>0) printf("prime\n");
else printf("composite\n");
return 0;}Листинг 2. Пример хорошо оформленного кода
#include <stdio.h>
int main(void){
int number_to_check,
is_prime = 1,
minimal_divisor = 2;
scanf("%d", &number_to_check);
for (int divisor = minimal_divisor; divisor < number_to_check; divisor++) {
if (number_to_check % divisor == 0) {
is_prime = 0;
break;
}
}
if (is_prime) {
printf("prime\n");
} else {
printf("composite\n");
}
return 0;
}Эти два кода работают совершенно одинаково и реализуют один и тот же алгоритм, но ответьте себе, какой из листингов более понятный и читаемый? В какой из них будет проще вносить изменения и правки? Думаю, что большинство из вас в обоих случаях выберет Листинг 2.
Давайте возьмём код из Листинга 1 и шаг за шагом приведём его в более читаемый и понятный вид, попутно изучая основные принципы написания чистого кода.
Структура кода
Основные наши помощники в структурировании кода программы: пустые строки, отступы и пробелы.
Принцип 1: Используйте пустые строки для разделения кода программы на смысловые части и блоки.
Как применить этот принцип к Листингу 1? Сперва, конечно, можно разделить пустой строкой подключение заголовочных файлов и определение функции main. Далее, в теле функции main тоже легко выделить три части: объявление и считывание переменных, обработка данных, вывод результата на экран. Получим следующую структуру программы:
- подключение заголовочных файлов
- функция main
- объявление и считывание переменных
- обработка введённых данных
- вывод результатаЛистинг 3. Добавление разделительных пустых строк в Листинг 1.
#include <stdio.h>
int main(void){
int N;
int tmp =1;
scanf("%d",&N);
for(int i=2;i<N;i++){
if(N%i==0)tmp=0;}
if(tmp>0) printf("prime\n");
else printf("composite\n");
return 0;}Теперь в коде появилась хотя бы как-то структура, но этого мало.
Принцип 2: Сдвигайте каждый вложенный блок относительно его "родителя" на несколько позиций вправо.
Например, тело функции main стоит сдвинуть на один уровень вправо, относительно основного текста программы. Чаще всего в качестве величины сдвига (размера отступа) рекомендуют использовать 4 или 8 пробелов. В этом уроке мы будем использовать отступы в 4 пробела
Листинг 4. Добавление структурных отступов в Листинг 3
#include <stdio.h>
int main(void){
int N;
int tmp =1;
scanf("%d",&N);
for(int i=2;i<N;i++){
if(N%i==0) tmp=0;}
if(tmp>0) printf("prime\n");
else printf("composite\n");
return 0;
}Самым явным сигналом о необходимости добавления структурного отступа является открывающая фигурная скобка {, т.к. в языке Си фигурные скобки используются для объединения нескольких инструкций в единый блок.
Есть три основных стиля расстановки фигурных скобок и отступов:
// первый стиль (K&R)
for (int i = 2; i < N; i++) {
if (N % i == 0) {
tmp = 0;
}
}
// второй стиль (BSD)
for (int i = 2; i < N; i++)
{
if (N % i == 0)
{
tmp = 0;
}
}
// третий стиль (GNU)
for (int i = 2; i < N; i++)
{
if (N % i == 0)
{
tmp = 0;
}
}Принцип 3: В рамках проекта придерживайтесь единого стиля расстановки фигурных скобок и структурных отступов.
Давайте добавим отступы и фигурные скобки в стиле K&R в Листинг 4.
Листинг 5. Добавление структурных отступов
#include <stdio.h>
int main(void){
int N;
int tmp = 1;
scanf("%d", &N);
for(int i=2;i<N;i++){
if(N%i==0){
tmp=0;
}
}
if(tmp>0){
printf("prime\n");
}else{
printf("composite\n");
}
return 0;
}Вообще говоря, здесь можно было бы вообще не использовать фигурные скобки у if-ов и for, т.к. внутри них содержится всего лишь одна инструкция. Но чтобы избежать ошибок и упростить себе жизнь в будущем, рекомендую: всегда добавляйте фигурные скобки в управляющих конструкциях, даже если их можно не использовать.
Теперь наш код стал структурированным. Осталось приправить его пробелами, чтобы сделать его ещё и более читаемым
Принцип 4: Добавляйте в код пробелы:
- после
, - после
;внутри заголовка циклаfor - между
)и{ - по обе стороны от бинарных операторов, например,
=,+,-,*,\и пр.
Листинг 6. Добавление дополнительных пробелов в Листинг 5
#include <stdio.h>
int main(void) {
int N;
int tmp = 1;
scanf("%d", &N);
for (int i = 2; i < N; i++) {
if (N % i == 0) {
tmp = 0;
}
}
if (tmp > 0) {
printf("prime\n");
} else {
printf("composite\n");
}
return 0;
}Отдельно остановимся на расстановке пробелов в арифметических выражениях. Иногда внутри них допустимо опускать пробелы вокруг бинарных операторов, чтобы нагляднее показать структуру выражения и/или приоритет операций. Операторы, имеющие более высокий приоритет, пишут без пробелов, в то время как операторы более низкого приоритета — с пробелами вокруг. Посмотрите на следующий листинг с примерами:
Листинг 7. Примеры оформления арифметических выражений
b*b + 4*a*c; // сначала умножение, потом сложение
(num_1*num_2) / (num_3*num_4); // сначала вычисляются выражения в числителе и знаменателе
(-b + determinant) / (2*a); // добавили пробелы вокруг + для улучшения читаемостиИмена переменных
Принцип 5: Давайте переменным говорящие, осмысленные имена. Не бойтесь использовать длинные и описательные имена.
Идея совета заключается в том, чтобы по имени переменной было понятно, что в ней хранится и для чего она используется.
Посмотрите, например, на листинг ниже.
Листинг 8. Пример плохого стиля именования переменных
#include <stdio.h>
int main() {
int s=0, d=0, h=0, m=0,
hind=24, secinh=3600;
scanf("%d", &s);
d = s / (hind * secinh);
s %= (hind * secinh);
h = s / secinh;
printf("%d\n", d);
printf("%d\n", h);
return 0;
}Тут без ста грамм не разберёшься. Чтобы понять, что хранится в переменных d и s, приходится постоянно обращаться к объявлениям secinh и hind. Здесь будет уместно вспомнить цитату из книги Роберта Мартина «Чистый код»:
нет худшей причины для выбора имени
c, чем та, что именаaиbуже заняты.
Вы можете возразить: «secinh — хорошее имя, оно обозначает sec in hour, то есть количество секунд в одном часе!». Да, так действительно можно расшифровать. Но очевидное ли это название для человека, который первый раз смотрит на код? Что помешало программисту использовать имя seconds_in_hour? Конечно, название стало длиннее, но зато теперь не возникает дополнительных вопросов. Код становится более читаемым и понятным, что значительно облегчает его поддержку и модификацию.
Приняв решение давать переменным осмысленные имена, мы сталкиваемся с ещё одной проблемой: «Как в имени, состоящем из нескольких слов, разделить слова между собой?»
Существует два основных подхода:
CamelCase и snake_case
Стиль CamelCase — каждое новое слово записывается с заглавной буквы. Название CamelCase связано с тем, что заглавные буквы создают визуальное впечатление “горбов”, как у верблюда. Вот несколько примеров имён переменных, записанных в стиле CamelCase:
DayInMonth,NumberTables,SecondsPassedFromFirstShot.
Существует разновидность этого стиля, где первую букву имени переменной делают строчной, а не заглавной, например:dayInMonth.Стиль snake_case — слова разделяются нижним подчёркиванием и все буквы обычно строчные. Такое название связано с тем, что нижние подчеркивания визуально напоминают изгибы змеи. Примеры имён переменных, записанных в стиле snake_case:
days_in_month,number_tables,seconds_passed_from_first_shot.
Выбор одного из этих стилей — вопрос личных предпочтений или соглашений, принятых в команде. Главное придерживаться выбранного стиля последовательно во всем проекте.
Хотя вы можете придумать свой особый способ именования переменных, всё же рекомендуется придерживаться одного из общепринятых стилей (CamelCase или snake_case), чтобы ваш код был понятен другим разработчикам
Вернёмся к Листингу 6 и внесём немного смысла в используемые имена переменных.
Листинг 9. Оформленная по всем принципам программа из Листинга 1.
#include <stdio.h>
int main(void){
int number_to_check,
is_prime = 1,
minimal_divisor = 2;
scanf("%d", &number_to_check);
for (int divisor = minimal_divisor; divisor < number_to_check; divisor++) {
if (number_to_check % divisor == 0) {
is_prime = 0;
break;
}
}
if (is_prime) {
printf("prime\n");
} else {
printf("composite\n");
}
return 0;
}Обратите внимание, что в заголовке цикла for число 2 было заменено на переменную minimal_divisor.
Принцип 6: Избегайте магических чисел в коде.
Для вас эти значения могут быть понятны и очевидны, но другие разработчики, читая ваш код, могут задаться вопросом о том, что это за число. Поэтому лучше если у него будет своё осмысленное и говорящее имя.
Может быть в этом конкретном примере это не очень наглядно, но вот вам другой пример, который буквально сегодня я обнаружил в решениях на Степике.
Листинг 10.
#include <stdio.h>
int main(void)
{
int N, drop;
double x, molecule;
scanf("%d", &N);
x = 8.317e+24;
drop = N * 4990;
molecule = N * x;
printf("%d %.4g", drop, molecule);
return 0;
}Даже видя условие задачи, я не сразу сообразил, что означают магические числа 4990 и 8.317e+24.
Итак, последовательно применив всего несколько простых принципов, мы превратили нечитаемую портянку из инструкций языка Си в хорошо структурированный и понятный код. Подобную работу над кодом — улучшение его читаемости и понятности без изменения функциональности — называют рефакторингом.
Комментарии — говорить или молчать?
Комментарии необходимы чтобы облегчить понимание кода другим программистам, которые будут его читать и поддерживать.
Раньше разработчики часто использовали комментарии для добавления в начало файла (“шапку”) информации об авторе, версии программы, истории изменений. Это помогало координировать работу в команде и отслеживать изменения. Но с появлением систем управления версиями (например, Git) необходимость в подобных “шапках” сильно сократилась.
Тем не менее, комментарии по-прежнему полезны для объяснения сложных алгоритмов, ссылок на стандарты или объяснения нестандартных решений и трюков. Однако комментарии, дублирующие очевидный код, не несут никакой пользы и только засоряют его.
Листинг 11. Плохой пример использования комментариев
#include <stdio.h>
int main() {
// Объявление переменных
int BatteryCapacityIn_mAh = 0, ConsuptionVoltage = 0, ConsuptionCurrentIn_mA = 0;
int TimeWorkingBattery_hours = 0, TimeWorkingBattery_minutes = 0;
printf("Enter Capacity battery, consuption in V and mA: "); // Вывод текста на экран
scanf("%d %d %d", &BatteryCapacityIn_mAh, &ConsuptionVoltage, &ConsuptionCurrentIn_mA); // Ввод данных
TimeWorkingBattery_hours = BatteryCapacityIn_mAh / (ConsuptionVoltage*ConsuptionCurrentIn_mA); // Вычисляем время работы в часах. Для этого ёмкость делим на расход
TimeWorkingBattery_minutes = (BatteryCapacityIn_mAh*60) / (ConsuptionVoltage*ConsuptionCurrentIn_mA); // Вычисляем время работы в минутах домножением на 60
printf ("\nWorking battery in hours: %d hours\nOR\nWorking battery in minutes: %d minutes\n\n", TimeWorkingBattery_hours, TimeWorkingBattery_minutes); // Вывод информации о времени работы батареи
return 0;
}В этой программе нет ни одного полезного комментария. Все комментарии лишь описывают очевидный код и не добавляют никакой новой информации, а наоборот, затрудняют чтение. Такие комментарии на зачётах по программированию используют нерадивые студенты, которым программу написал друг или генеративный AI. Им они действительно нужны, т.к. для них комментируемый код зачастую не является очевидным. =)
Принцип 7: Избегайте избыточных и тривиальных комментариев, которые не несут полезной информации.
В целом, старайтесь минимизировать использование комментариев. Если часть кода требует подробного объяснения, возможно, лучше переписать её, чтобы сделать более понятной, чем оставлять сложный код и компенсировать это комментариями.
Чтобы показать разумное использование комментариев, давайте усовершенствуем программу из Листинга 9 следующим образом:
Листинг 12. Добавляем в Листинг 9 математический трюк для оптимизации перебора
#include <stdio.h>
int main(void){
int number_to_check,
is_prime = 1,
minimal_divisor = 2;
scanf("%d", &number_to_check);
/*
математическая оптимизация. Перебираем не все делители, а до sqrt(number_o_check),
т.к. каждому делителю < sqrt(...) соответствует делитель > sqrt(...).
Например, для числа 100: делителю 5 < sqrt(100) соответствует делитель 20 > sqrt(100)
*/
for (int divisor = minimal_divisor; divisor * divisor <= number_to_check; divisor++) {
if (number_to_check % divisor == 0) {
is_prime = 0;
break;
}
}
if (is_prime) {
printf("prime\n");
} else {
printf("composite\n");
}
return 0;
}В этой программе комментарий уместен, т.к. без него используемый математический трюк может быть непонятен другим программистам.
Резюмируя, выпишем основные принципы написания чистого кода:
- Разделяйте смысловые части и блоки программы пустыми строками.
- Используйте отступы для вложенных блоков.
- Используйте пробелы для улучшения читаемости длинных конструкций.
- Давайте переменным осмысленные имена.
- Используйте единый стиль расстановки отступов, фигурных скобок и именования переменных.
- Избегайте использования "магических" чисел.
- Избегайте избыточных и тривиальных комментариев.
- Чтобы код был понятен международному сообществу разработчиков, пишите комментарии на английском языке.
Дополнительные материалы
Существует Linux стиль в расстановке фигурных скобок и отступов. В нём для функций применяется BSD стиль (фигурные скобки на новой строке), а для управляющих конструкций языка, например,
for,if,switch,whileи пр. используется K&R стиль (открывающая фигурная скобка на той же строке). Именно этого стиля я придерживаюсь при оформлении большинства листингов и примеров в материалах Курса.Крупные компании и проекты обычно имеют собственные правила, которые описывают различные нюансы оформления кода программ. Такие руководства называют стайлгайдами (style guide). Использование единых правил по оформлению кода делает его единообразным, ускоряет разработку и снижает когнитивные нагрузки на разработчиков, т.к. им не приходится каждый раз перестраиваться под различные стили написания кода внутри одного проекта. Естественно, использование стайлгайдов также упрощает и дальнейшую поддержку кода.