«Вкус — это способность судить о прекрасном»
И. Кант
Дирк Хондел, один из тех, кто стоял у истоков Linux, однажды сказал о создателе Linux Линусе Торвальдсе: «Линус не только блестящий программист: у него хороший вкус. Торвальдс находит простые и разумные пути решения проблем, умеет всё «разложить по полочкам». Сложные вещи он делает простыми. По-моему, это и есть главное отличие превосходного программиста от просто хорошего».
В недавнем интервью, примерно на 14-й минуте, Линус Торвальдс коснулся темы «хорошего вкуса в программировании». Хороший вкус? Ведущий попросил его остановиться на этом подробнее, и Линус, пришедший не с пустыми руками, показал пару слайдов.
Сначала был продемонстрирован пример плохого вкуса в программировании, для того, чтобы на его фоне лучше было видно достоинства кода более качественного.
Пример плохого вкуса в программировании
Это – функция, написанная на C, которая занимается удалением объектов из связанного списка. Она состоит из 10 строк кода.
Линус привлёк внимание к управляющей конструкции if в нижней части функции. Именно этим фрагментом он был особенно недоволен.
Я поставил видео на паузу и внимательно рассмотрел слайд. Я совсем недавно писал что-то подобное. По сути, Линус сказал, что у меня плохой вкус. Проглотив обиду, я продолжил смотреть видео.
Я уже сталкивался с тем, что Линус объяснял аудитории. А именно, речь шла о том, что при удалении объектов из связанного списка нужно рассмотреть два случая. Первый – когда объект находится где-нибудь в середине списка. Второй – для объекта в начале списка. Такой подход вынуждает использовать конструкцию if и приводит к написанию безвкусного кода.
Но, если сам Линус признаёт необходимость использования условного оператора, почему же такой подход его не устраивает?
Дальше он показал аудитории второй слайд. Это был пример той же функции, но на этот раз написанной со вкусом.
Пример хорошего вкуса в программировании
То же самое, что в предыдущем примере делалось в десяти строк кода, теперь укладывается в четыре.
Но сама по себе длина текста программы не особенно важна. Важен подход, который приводил к появлению условного оператора в конце первого примера. В новом коде дополнительные условия проверять не нужно. Код был переработан так, что и для удаления элемента из середины списка, и для удаления первого элемента, применяется один и тот же подход.
Линус объяснил новый код, сказал, что самое главное заключается в устранении пограничного случая, после чего разговор переключился на другую тему.
Размышления о хорошем вкусе в программировании
Какое-то время я размышлял над примером. Линус был прав. Второй фрагмент гораздо лучше. Если бы это был тест на различение хорошего и плохого вкуса в программировании, я бы этот тест провалил. Мысль о том, что можно обойтись без этого злосчастного условия, никогда не приходила мне в голову. И я не раз писал подобное, так как часто работаю со связанными списками.
Пожалуй, главная ценность вышеописанного примера даже не в том, что он демонстрирует хороший способ удаления элементов из связанного списка. Главное здесь то, что этот пример заставляет размышлять о том, что код, который ты написал, реализации небольших алгоритмов, разбросанные по программе, можно улучшить такими путями, о которых ты и не подозревал.
Вот на эту идею я и обратил особое внимание, когда решил пересмотреть тексты своего свежего проекта. Возможно, это судьба, но моя программа тоже написана на C.
Насколько я понимаю, в центре внимания вопроса о хорошем вкусе в программировании лежит устранение пограничных случаев, которые имеют тенденцию проявляться в коде как условные операторы. Хороший вкус в программировании выражается, таким образом, в сокращении количества условий, которые приходится проверять.
Хочу рассказать об одном удачном примере улучшения моего кода.
Инициализация краёв сетки со вкусом
Ниже показан алгоритм, который я написал для того, чтобы инициализировать элементы вдоль краёв сетки, которая представлена в виде многомерного массива: grid[rows][cols]
.
Цель этого кода заключалась лишь в том, чтобы инициализировать значения для элементов, которые располагаются по краям – то есть, меня здесь интересовали верхняя и нижняя строки, и правый и левый столбцы.
Для того, чтобы это сделать, я, в исходном варианте программы, прошёлся в цикле по каждому элементу сетки, и, используя условный оператор, проверил, находится ли он на краю. Вот как это выглядело:
for (r = 0; r < GRID_SIZE; ++r) {
for (c = 0; c < GRID_SIZE; ++c) {
// Верхний край
if (r == 0)
grid[r][c] = 0;
// Левый край
if (c == 0)
grid[r][c] = 0;
// Правый край
if (c == GRID_SIZE - 1)
grid[r][c] = 0;
// Нижний край
if (r == GRID_SIZE - 1)
grid[r][c] = 0;
}
}
Хотя всё работало как надо, было понятно, что код далеко не идеален. А именно, вот основные проблемы этого фрагмента:
- Код слишком сложно устроен. Использование четырёх условных операторов в двух вложенных циклах выглядит неуклюже.
- Код неэффективен. При условии, что переменная GRID_SIZE установлена в значение 64, тело внутреннего цикла выполнится 4096 раз только для того, чтобы найти 256 элементов на краях.
Линус, вероятно, согласился бы с тем, что этот код нельзя отнести к образцам хорошего вкуса.
Повозившись какое-то время с программой, я смог уменьшить сложность алгоритма, реализация которого теперь содержала лишь один цикл for, который содержал четыре условия. Это было небольшое улучшение в плане уменьшения сложности структуры кода, но серьёзное – в производительности. Теперь выполняется лишь 256 проходов цикла, один для каждого элемента, расположенного на краю. Вот, как выглядел тот же фрагмент после улучшения.
for (i = 0; i < GRID_SIZE * 4; ++i) {
// Верхний край
if (i < GRID_SIZE)
grid[0][i] = 0;
// Правый край
else if (i < GRID_SIZE * 2)
grid[i - GRID_SIZE][GRID_SIZE - 1] = 0;
// Левый край
else if (i < GRID_SIZE * 3)
grid[i - (GRID_SIZE * 2)][0] = 0;
// Нижний край
else
grid[GRID_SIZE - 1][i - (GRID_SIZE * 3)] = 0;
}
Стало лучше? Да. Но выглядит всё это просто отвратительно. Этот код не из тех, которые можно понять с первого взгляда. Только одно это заставило меня двигаться дальше.
Я продолжил экспериментировать, задался вопросом о том, а можно ли ещё что-то улучшить. Ответ был однозначным: «Да, можно». И то, к чему я в итоге пришёл, было настолько поразительно просто и элегантно, что я, честно говоря, не мог поверить в то, что для того, чтобы до этого додуматься, мне пришлось потратить столько времени.
Вот то, что у меня получилось. Здесь лишь один цикл и никаких условных операторов. Более того, тело цикла исполняется лишь 64 раза. Этот вариант значительно проще и производительнее первого.
for (i = 0; i < GRID_SIZE; ++i) {
// Верхний край
grid[0][i] = 0;
// Нижний край
grid[GRID_SIZE - 1][i] = 0;
// Левый край
grid[i][0] = 0;
// Правый край
grid[i][GRID_SIZE - 1] = 0;
}
В этом коде в каждой итерации цикла инициализируется четыре разных граничных элемента. Код просто устроен и весьма эффективен в плане производительности. Его легко читать. Этот вариант не идёт ни в какое сравнение с первым и даже со вторым.
В итоге результатами я остался абсолютно доволен.
Есть ли у меня вкус к программированию?
И что же, теперь я программист, код которого отвечает правилам хорошего вкуса?
Мне хочется надеяться, что так оно и есть, но не из-за того, что я смог переделать неудачный фрагмент своей программы, который показал выше, да и другие тоже, которые в статью не включил. Всё дело в том, что проявление хорошего вкуса в программировании – это нечто большее, нежели некий фрагмент текста. Линус и сам говорил, что пример, который он привёл, слишком мал для того, чтобы должным образом проиллюстрировать его точку зрения.
Я полагаю, что Линус имел в виду то, что разработчики, обладающие «хорошим вкусом к программированию», отличаются от других тем, что они уделяют время на осмысление того, что они создают, перед тем, как начинают писать код.
Они определяют границы компонентов, с которыми работают, и то, как эти компоненты взаимодействуют. Они стремятся удостовериться, в том, что всё хорошо друг с другом сочетается, стараются добиться изящества кода и процесса его исполнения.
Результат такого подхода похож на код, который привёл Линус, да и на мой тоже, только в другом, более крупном, масштабе.
А как вы можете применить концепцию «хорошего вкуса» в своих проектах?
Автор: RUVDS.com