Когда программный код вызывает восхищение?

ra0syx2ovvhaqig6cdsntx297cw.png

Тема идеального кода нередко вызывает полемику в среде матерых программистов. Тем интереснее было заполучить мнение директора по разработке Parallels RAS Игоря Марната. Под катом его авторский взгляд по заявленной теме. Enjoy!

iykyzgb9kkv_lvpvfplcfbbfqmm.png

В качестве введения хотелось бы остановиться на вопросе, почему же я вообще решил написать эту небольшую статью. Перед ее написанием я задал вопрос из заголовка нескольким разработчикам. С большинством из ребят довелось поработать больше пяти лет, с некоторыми чуть меньше, но их профессионализму и опыту доверяю безоговорочно. У всех стаж промышленной разработки больше десяти лет, все работают в российских и международных компаниях, производителях программного обеспечения. 
 
Некоторые коллеги затруднились с ответом (кое-кто думает до сих пор), другие назвали один-два примера сразу. Тем, кто привёл примеры, я задал уточняющий вопрос — «Что, собственно, вызвало это восхищение?». Ответы соответствовали результатам следующего этапа моего небольшого исследования. Я поискал в сети ответы на этот вопрос в разных формулировках, близких к заголовку статьи. Все статьи отвечали примерно таким же образом, каким ответили мои товарищи.
 
Ответы разработчиков, как и формулировки найденных статей, относились к читаемости и структуре кода, изяществу логических конструкций, использованию всех возможностей современных языков программирования и следованию определённому стилю оформления.
 
Когда же я задал вопрос о «божественном коде» себе, то ответ всплыл незамедлительно, из подсознания. Я сразу же подумал о двух примерах кода, с которыми работал давно (больше десяти лет назад), но чувство восхищения и некоторого благоговения испытываю до сих пор. Обдумав причины восхищения каждым из них, я сформулировал несколько критериев, о которых речь пойдёт ниже. На первом примере я остановлюсь вскользь, второй же хотелось бы разобрать более подробно. Кстати, в разной степени, все эти критерии рассмотрены в настольной книге каждого разработчика «Совершенный код» от Стива Макконнелла, но эта статья заметно короче.

Пример из 90-х


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

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

Linux всему голова!


Второй пример совершенного кода хотелось бы разобрать более подробно. Это код ядра Linux. Код, который на момент написания статьи управляет работой 500 суперкомпьютеров из top 500, код, который работает в каждом втором телефоне в мире и который управляет большей частью серверов в сети Интернет.
 
Рассмотрим для примера файл  memory.c из ядра Linux, который относится к подсистеме управления памятью. 

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

static void tlb_remove_table_one(void *table)
{
        /*
         * This isn't an RCU grace period and hence the page-tables cannot be
         * assumed to be actually RCU-freed.
         *
         * It is however sufficient for software page-table walkers that rely on
         * IRQ disabling. See the comment near struct mmu_table_batch.
         */
        smp_call_function(tlb_remove_table_smp_sync, NULL, 1);
        __tlb_remove_table(table);
}

2. Комментариев в коде не слишком много, но те, что есть, обычно полезны. Они, как правило, описывают не действие, которое и так очевидно из кода (классический пример бесполезного комментария — «cnt++; // increment counter»), а контекст этого действия — почему здесь делается то, что делается, почему это делается так, почему здесь, с какими предположениями это используется, с какими другими местами в коде это связано. К примеру:  

/**
 * tlb_gather_mmu - initialize an mmu_gather structure for page-table tear-down
 * @tlb: the mmu_gather structure to initialize
 * @mm: the mm_struct of the target address space
 * @start: start of the region that will be removed from the page-table
 * @end: end of the region that will be removed from the page-table
 *
 * Called to initialize an (on-stack) mmu_gather structure for page-table
 * tear-down from @mm. The @start and @end are set to 0 and -1
 * respectively when @mm is without users and we're going to destroy
 * the full address space (exit/execve).
 */
void tlb_gather_mmu(struct mmu_gather *tlb, struct mm_struct *mm,
                        unsigned long start, unsigned long end)

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

/*
 * demand-loading started 01.12.91 - seems it is high on the list of
 * things wanted, and it should be easy to implement. - Linus
 */
 
/*
 * Ok, demand-loading was easy, shared pages a little bit tricker. Shared
 * pages started 02.12.91, seems to work. - Linus.
 *
 * Tested sharing by executing about 30 /bin/sh: under the old kernel it
 * would have taken more than the 6M I have free, but it worked well as
 * far as I could see.
 *
 * Also corrected some "invalidate()"s - I wasn't doing enough of them.
 */

3. В коде ядра используются специальные макросы для проверки данных. Они также используются для проверки контекста, в котором работает код. Функциональность этих макросов похожа на стандартный assert, c той разницей, что разработчик может переопределить действие, которое выполняется в случае истинности условия. Общий подход к обработке данных в ядре — проверяется всё, что приходит из user space, в случае ошибочных данных возвращается соответствующее значение. При этом может использоваться WARN_ON для выдачи записи в лог ядра. BUG_ON же обычно весьма полезны при отладке нового кода и запуске ядра на новых архитектурах. 
 
Макрос BUG_ON обычно вызывает печать содержимого регистров и стека и либо останавливает всю систему, либо процесс, в контексте которого произошёл соответствующий вызов. Макрос WARN_ON  просто выдаёт сообщение в лог ядра в случае истинности условия. Есть также макросы WARN_ON_ONCE и ряд других, функциональность которых понятна из названия.

void unmap_page_range(struct mmu_gather *tlb,
….
         unsigned long next;
 
        BUG_ON(addr >= end);
        tlb_start_vma(tlb, vma);
 
 
int apply_to_page_range(struct mm_struct *mm, unsigned long addr,
…
        unsigned long end = addr + size;
        int err;
 
        if (WARN_ON(addr >= end))
                return -EINVAL;

Подход, при котором данные, полученные из ненадежных источников, проверяются перед использованием, и реакция системы на «невозможные» ситуации предусмотрена и определена, заметно упрощает отладку системы и её эксплуатацию. Можно рассматривать этот подход как реализацию принципа fail early and loudly. 

4. Все основные компоненты ядра предоставляют пользователям информацию о своём состоянии через простой интерфейс, виртуальную файловую систему /proc/.

К примеру, информация о состоянии памяти доступна в файле /proc/meminfo

user@parallels-vm:/home/user$ cat /proc/meminfo
MemTotal:        2041480 kB
MemFree:           65508 kB
MemAvailable:     187600 kB
Buffers:           14040 kB
Cached:           246260 kB
SwapCached:        19688 kB
Active:          1348656 kB
Inactive:         477244 kB
Active(anon):    1201124 kB
Inactive(anon):   387600 kB
Active(file):     147532 kB
Inactive(file):    89644 kB
….

Информация, приведённая выше, собирается и обрабатывается в нескольких исходных файлах подсистемы управления памятью. Так, первое поле MemTotal, это значение поля totalram структуры sysinfo, которая заполняется функцией si_meminfo файла page_alloc.c.
 
Очевидно, организация сбора, хранения и предоставления пользователю доступа к такой информации требует усилий от разработчика и некоторых накладных расходов от системы. При этом, польза от наличия удобного и простого доступа к таким данным неоценима, как в процессе разработки, так и эксплуатации кода. 
 
Разработку практически любой системы стоит начинать с системы сбора и предоставления информации о внутреннем состоянии вашего кода и данных. Это очень поможет в процессе разработки и тестирования, и, в дальнейшем, в эксплуатации.

Как сказал Линус, «Bad programmers worry about the code. Good programmers worry about data structures and their relationships».

5. Весь код читается и обсуждается несколькими разработчиками перед коммитом. История изменений исходного кода записана и доступна. Изменения любой строки можно проследить вплоть до её возникновения — что менялось, кем, когда, почему, какие вопросы при этом обсуждались разработчиками. К примеру, изменение https://github.com/torvalds/linux/commit/1b2de5d039c883c9d44ae5b2b6eca4ff9bd82dac#diff-983ac52fa16631c1e1dfa28fc593d2ef в коде memory.c, вдохновлено  багом  https://bugzilla.kernel.org/show_bug.cgi? id=200447 в котором сделана небольшая оптимизация кода (вызов включения защиты памяти от записи не происходит, если память уже защищена от записи). 
 
Разработчику, работающему с кодом, всегда важно понимать контекст вокруг этого кода, с какими предположениями код создавался, что и когда менялось, чтобы понимать, какие сценарии могут быть затронуты теми изменениями, который собирается делать он сам.

6. Все важные элементы жизненного цикла кода ядра документированы и доступны, начиная со стиля кодирования и заканчивая содержимым и расписанием выпуска стабильных версий ядра. Каждый разработчик и пользователь, который хочет работать с кодом ядра в том или ином качестве, имеет для этого всю необходимую информацию.
 
Эти моменты показались мне важными, в основном, они определили моё восторженное отношение к коду ядра. Очевидно, список весьма краткий и может быть расширен. Но перечисленные выше пункты, на мой взгляд, относятся к ключевым аспектам жизненного цикла любого исходного кода с точки зрения разработчика, работающего с этим кодом. 
 
Что бы мне хотелось сказать в заключение. Разработчики ядра — умные и опытные, они добились успеха. Доказано миллиардами устройств под управлением Linux
 
Будьте как разработчики ядра, используйте лучшие практики и читайте Code Complete!

З.Ы. Кстати, а какие критерии идеального кода лично у вас? Поделитесь мыслями в комментариях. 

© Habrahabr.ru