Если совсем базово, то можно выделить три уровня красоты кода:
- Визуальный. Это как раз все про coding conventions, правильные переменные, оформление и прочее.
- Восприятие кода. Про ощущения, которые возникают у людей, работающих с вашим кодом.
- Продуманность архитектуры. Это тоже критично и тоже относится именно к красоте кода.
Сразу же оговоримся, что мы обсуждаем код, выполняющий ту работу, которую должен, и делающий это правильно. Нерабочий код оценивать с точки зрения красоты не имеет никакого смысла.
А теперь давайте по каждому пункту отдельно.
Визуальная красота кода
Так вот, о красоте. Здесь нужно исходить из того, что вы как программист не работаете в вакууме, вы не какая-то одиночная боевая единица в закрытом контуре. На работе и в командах у вас есть коллеги. Если вы работаете один, то ваш код все равно, скорее всего, кто-то использует, работает с ним, поддерживает его и так далее.
Чтобы избежать субъективности и вкусовщины в этом деле, в IT-компаниях создают coding conventions, стандарт оформления кода. Это весьма фиксированный набор правил, определяющий, как именно должен выглядеть код — как ставить скобки, использование пробелов и табуляции, в общем, чтобы код, созданный в компании, выглядел одинаково. В хорошем смысле этого слова.
Если не следовать этим правилам, получится, что одна функция в коде будет написана одним стилем, вторая — другим. Это банально будет трудно читать. Тут как с книгой. Представьте, что вы читаете хорошую по содержанию книгу, но у нее половина страницы набрана Times new roman 14, а половина — comic sans 16, еще и с хаотичным курсивом и заглавными вразнобой. Вроде бы читать-то можно, смысл не теряется. Так же и с кодом. Работать он будет. Но осадочек останется.
Поэтому стандарты оформления и определяют, что именно (и почему) считается красивым кодом с точки зрения его оформления в той или иной компании. Существуют стайлгайды, которые гиганты пишут для себя. Часто они выложены открыто, к примеру, стайлгайд от Google лежит на Github. Вы можете взять его за основу и, если разделяете его положения, использовать его для создания собственного. А можно вообще взять в текущем виде и сразу начать использовать у себя.
Восприятие кода и его архитектура
Помните, что код должен быть красиво организован. Под этим понимается и процесс выбора названий для ваших переменных, и размеры ваших классов, функций и методов, и частота использования условных выражений. Ведь чем больше в коде различных if else и switch case, тем менее красивым он становится, превращаясь в нагромождение условий и частных случаев. И это не чисто визуальная проблема — его будет сложнее понимать, дорабатывать и поддерживать.
Кстати, о coding conventions в этом ключе. Важно помнить, что правила просто не могут не включать всего и вся, например, тех же советов по использованию условий может и не быть, но хороший программист сам подспудно не допустит в своем коде подобного. Либо опытный наставник в процессе ревью поможет вам исправить мелкие шероховатости и сделать код красивее.
Отчасти из-за невозможности чётко регламентировать всё и вся в отрасли возникло явление так называемого «вонючего кода», code smells. Когда вроде есть перед вами код, и даже всем правилам компании он соответствует, но что-то в нем таки не то. Чтобы этого избежать, полезно проводить рефакторинг.
// The function CalcElements counts how many elements is equal to arg
int CalcNumOfVariousElements(const vector<int>& v, int condition) {
int i = 0;
int count = 0;
vector<int>& tmp_v = const_cast <vector<int>&>(v);
while (i != 10) {
if (v[i] != condition) {
tmp_v[i] = condition;
count++;
//ProcceedElementsCheck(v[i]);
}
i++;
}
return count;
}
Этот код вполне соответствует coding convention. Но у него есть много проблем, заставляющих опытного программиста «учуять» неладное.
- Комментарий над функцией явно устарел. Такие комментарии сбивают с толку и могут быть даже опасны. На них нельзя положиться.
- Название функции не соответствует тому, что в функции происходит.
- Переменная типа int названа словом «condition», что выглядит сомнительно с точки зрения логики.
- В названии функции заявлено, что она должна элементы считать. Вряд ли пользователь ожидает, что с элементами вектора что-то произойдет, тем более, что контейнер передается как константная ссылка. Но внутри функции константность убирается и элементы контейнера меняются! Пользователь вряд ли это ожидает, и вызов такой функции потенциально небезопасен.
- Magic number 10, про которое никто ничего не знает, и, вероятно, в момент, когда нужно будет его поменять, никто про него не вспомнит, и в программе появится баг.
- Мёртвый код (закомментированный вызов ProcceedElementsCheck) мешает чтению и навигации по коду проекта. Возникает ощущение чтения черновика.
Вероятно, опытный глаз может заметить тут ещё множество недочетов.
Следующие примеры будут менее очевидными.
Sphere makeSphere(double x, double y, double z, double radius, Color color);
Отличная функция, но у неё целых 5 аргументов. Ее вызов может выглядеть следующим образом:
Sphere sphere = makeSphere(CalcX(1, 2), other_sphere.GetY(), CalcZ(3, 2), 0.3241, SearchForColor(list_of_colors));
Чем больше аргументов, тем сложнее концентрироваться на том, какой параметр к чему относится. Дописав вызов такой функции до конца, чувствуешь, что разгрузил вагон. Читать такое тоже непросто. А запутаться и передать аргументы не в том порядке, породив баг — элементарно.
bool IsOKPressed(Screen scr) {
for (int i = 0; i < scr.GetBtnList().size(); ++i) {
if (BUTTON_OK == scr.GetBtnList()[i].GetType() && scr.GetBtnList()[i].IsPressed()) {
ShowOKScreen();
return true;
}
}
return false;
}
С этим примером всё в целом неплохо, кроме того, что функция IsOKPressed имеет побочный эффект. Если кнопка Ok была нажата, эта функция покажет соответствующий экран. Из-за этого функция IsOKPressed может быть вызвана только в определенный момент. Несвоевременный вызов может привести к тому, что действия, которые необходимо сделать до показа нового экрана пользователю, сделаны не будут.
Ещё один небольшой пример, который можно улучшить:
void CopyItemList(ItemList item_list1, ItemList item_list2) {
for (int i = 0; i < item_list1.Size(); ++i) {
item_list2[i] = item_list1[i];
}
}
Эта функция гораздо лучше бы смотрелась, если бы у переменных были бы имена source и destination.
Нумерация переменных не дает никакой информации о том, какой функционал они будут выполнять. То же самое можно сказать про префиксы типа Data, Info или Details. Разница между классами ClientData, ClientInfo и ClientDetails не будет понятна никому, кроме создателя этих классов.
Когда программист-джуниор немного поучился и почувствовал себя уверенно, он рискует начать считать, что ну он-то точно никогда такой код не напишет. Эффект Даннинга — Крюгера как раз об этом.
«Вы же специально сейчас дали пример такой ужасной функции», — подумает такой начинающий программист. Но вот пример из реально существующего проекта (небольшой дисклеймер — все переменные переименованы и любое совпадение с реальными переменными — просто совпадение):
do {
//search for next record (can be before or after last record)
while ((result = GetRecord(searchDirection == 1? lastIndex++: lastIndex--, Rec)) != false && Rec->field.InTime == 0);
} while(loopIndex--);
Этот код работает. Он даже соответствует coding convention внутри компании, где он был написан. Но запах этого кода явно призывает к рефакторингу. В некоторых, особо запущенных случаях проще написать заново, чем пытаться понять, что же хотел этим сказать автор.
А еще больше хороших примеров плохого кода можно посмотреть в книге «Чистый код», которую я бы рекомендовала к прочтению всем, вне зависимости от стажа.
Допустим, вы написали какой-то код. Прошло полгода, за это время вы успели поучаствовать еще в ряде проектов и написать, прямо скажем, не одну сотню строк нового кода. Стоит ли, выбрав время, оглянуться назад и посмотреть на свой старый код, отрефакторить его? Ответ простой — обязательно.
Во-первых, это повысит качество самого кода, а вы научитесь замечать детали, которые надо исправлять и которых стоит избегать в будущем. Во-вторых, с ненулевой вероятностью вам будет нужно время от времени рефакторить и чужой код — сможете набить руку на своем же старом. В блоке полезных ссылок в конце поста вы найдете ссылку на отличный ресурс о том, как делать правильный рефакторинг. В-третьих, вы избавите старый код от того самого запаха.
Давайте немного подробнее про продуманность архитектуры. Здесь я имею в виду то, насколько ваш код грамотно продуман с точки зрения архитектуры. Бывают ситуации, когда код полностью соответствует первым двум пунктам — и красиво, и по правилам, и пахнет отлично. Вроде, все здорово, да?
А потом вы идете и пытаетесь немного поправить класс или что-то поменять внутри существующей фичи — а код просто рассыпается у вас под курсором. Просто потому, что его архитектура плохо продумана.
Красивый код — это код, который удобно поддерживать. Почитать про SOLID и о других важных аспектах красоты кода я советую в книге «Чистый код» Роберта Мартина.
Зачем джуниору писать красиво
Во-первых, если красивый код станет для вас привычкой, составляющей тот базис знаний, с которым вы входите в профессию, то вам будет гораздо проще в дальнейшем. Вы просто привыкнете писать красиво и будете воспринимать это как самый простой и подсознательный акт кодинга.
Конечно, можно начать красиво писать, будучи мидлом или сениором. Но тут вам, скорее всего, придется переучиваться и немного менять собственные подходы к разработке (которые тоже уже стали привычкой). А как показывает практика, это сложнее. Плюс не все люди в принципе готовы менять привычки.
Если же сразу решить для себя, каким именно вы хотите видеть код, какой стайлгайд кажется вам максимально близким, как вы станете называть методы и почему — дальше будет гораздо проще.
А еще это может принести джуниору пользу на собеседовании. Когда рекрутеры или специалисты на техническом собеседовании будут смотреть на код двух разных джуниоров, с большой долей вероятности предпочтение отдадут тому, у которого код красивее.
Помните, что именно джуниоры продолжают активно учиться в процессе работы. Джуниор в первый месяц испытательного срока и джуниор полгода спустя — это два разных человека с профессиональной точки зрения и второй чаще всего смотрит на код первого и думает — как я вообще мог такое написать вот этими руками.
Мидлы и сениоры, если им хочется совершенствовать свои навыки, тоже могут продолжать осваивать что-то новое. Но все равно в их работе всегда будет больше именно самой работы, нежели активного впитывания новых знаний.
Красота в динамике
Критерии красоты кода нельзя высечь в камне и показывать вновь прибывшим в отрасль как истину в последней инстанции. Это понятие меняется со временем. То, как люди программировали и писали код всего 10-20 лет назад, и то, как это делают сегодня — это очень разные вещи.
Поэтому полезно держать руку на пульсе и смотреть, как и что сейчас пишут, как создают современные ресурсы. Неоценимую помощь тут оказывает open source — просто открывайте интересующий вас свежий популярный проект и смотрите, как его делают. На чем. Почему именно так. Задавайте вопросы, оставляйте комментарии. Не стесняйтесь открывать сообществу собственные проекты и получать по ним обратную связь.
Это отличный стимул и возможность самосовершенствоваться.
Чеклист
А теперь давайте соберем все то, что выше, в небольшой чеклист, который поможет вам улучшать качество кода.
1. Следуйте coding conventions
Даже если вы не работаете на компанию, а фрилансите. Даже если просто учитесь. Этот этап как раз и закладывает привычку писать красиво.
2. Используйте специальные инструменты
Для корректировки кода под конкретный стайлгайд существует множество различных плагинов, скриптов и шаблонов для разных сред программирования. IDE успешно автоматизируют эту рутину — изучите процесс: и время сэкономите, и инструмент освоите.
3. Пусть ваш код говорит сам за себя
Делайте его очевидным. Коллега должен посмотреть на код и понять, как все работает, даже если вы не оставили ни одного комментария. Понятные названия переменных, классов, методов.
А вот если у вас в коде комментарий к каждой строке, без которого непонятно, что происходит — это повод задуматься.
4. Изучите принципы SOLID и следуйте им
Чтобы ваш код не рассыпался в прах при попытках коллег внести в него изменения.
5. Если можете, отправляйте свой код на код-ревью
Внимательно отнеситесь к правкам и комментариям, которые вам оставят. Часто на код-ревью можно выяснить, что то, что вы считали «элегантным решением», оказывается чем-то чудовищным в глазах других программистов.
6. Оставляйте время для рефакторинга
И, что важно, после проведенного рефакторинга, когда ваш код уже стал новым и красивым, прогоните тесты еще разок. Есть вероятность, что код стал красивым, но что-то перестало работать. И будет лучше, если вы заметите это на тесте. А не коллеги на работе. Или вообще пользователь вашего сервиса на проде.
7. Читайте код новых проектов в open source
Поможет вам быть в курсе новых практик и подходов.
Полезные книги и ресурсы
Роберт Мартин «Чистый код» (здесь же есть про SOLID) [бумажная книга] [электронная книга]
Мартин Фаулер «Рефакторинг. Улучшение существующего кода» [бумажная книга]
Отличный ресурс о рефакторинге — refactoring.guru/ru/refactoring
Инструменты автоматизации:
Plug-in Beautifier для различных IDE
clang-format
Google C++ Style Guide
hd_keeper
Дальше не читал.
WellD
Почему?
qark
Возможно, из-за следования критерию отбора статей для прочтения.
hd_keeper
> Яндекс
> С++
> МАША
Вот если бы автора звали Брайан, Деннис, Бьярне, или, на худой конец, хотя бы Герберт…
300KpS
Ну не читай, пройди мимо и читай то, что интересно тебе. Зачем в комменты-то идти, если не читал?
hd_keeper
Тред не читай — сразу отвечай!