10 принципов хорошего программного кода, который устроит всех
Чтобы от программного кода не отмахивались собеседующие или коллеги, он должен быть удобоваримым. Как написать «конфетку», которая понравится всем?
Роберт Мартин когда-то сказал, что единственным допустимым измерением качества кода является «Что за…».
Позвольте объяснить. Всякий раз, когда на глаза попадается какой-либо код, возникает одна из трех эмоций:
- «Что за…», произнесенное с отвращением – этот код вообще не нужен.
- «Что за…», выражающее восхищение – «Эй, а ведь этот парень действительно крут!»
- «Что за…» с раздражением – в представленном коде невозможно разобраться.
Так что отвечает за реакцию на код? Это чистота кода. Писать чистый и предельно понятный код – признак настоящего профессионала. Как этого добиться?
Такое мастерство создано из двух главных составляющих: знания и работы. Знание учит вас шаблонам, принципам, практике и эвристике, которые необходимы для постоянного совершенствования. Но это знание должно быть подтверждено практикой, без которой оно попросту не будет закрепляться.
Так что красота программного кода – это не так уж просто, и если собеседующему что-то не понравилось, не расстраивайтесь: помните, что это все придет с опытом. В этой статье мы собрали десять принципов по-настоящему хорошего кода.
«Могу ли я узнать ваше имя?»
В программировании имена повсюду. Мы называем функции, классы, аргументы, пакеты и т. д. Иногда мы думаем что-то вроде: «И так понятно, что text – это текстовое поле. Зачем мудрить?» Однако взглянув на код или часть программного кода через неделю, две или месяц, натыкаемся на абракадабру, в которой непонятно что непонятно за что отвечает.
Допустим, вы написали небольшую графическую часть на Java. Что можно сказать о происходящем из представленного ниже программного кода?
setTitle(text1); setSize(550,360); setVisible(true); text2.setEditable(false); try { List<String> text = Files.readAllLines(Paths.get("text.txt")); name = text.get(0)+":"; name2.setText(name); } catch (IOException e) { e.printStackTrace(); }
В коде происходит нечто странное. Давайте немного изменим его:
setTitle(title); //Здесь мы видим заголовок setSize(550,360); setVisible(true); textAreaMessage.setEditable(false); //Здесь уже JTextArea, etc. try { List<String> mass = Files.readAllLines(Paths.get("text.txt")); name = mass.get(0)+":"; labelName.setText(name); } catch (IOException e) { e.printStackTrace(); }
Имя любой переменной, функции или класса должно отвечать на три вопроса: зачем это нужно, что делает и как используется. Выбор хороших имен поначалу требует времени, но в дальнейшем вы сэкономите намного больше. Тщательно подбирайте названия, и все, кто прочтет ваш код, будет вам благодарен.
Не бойтесь разбивать код на составляющие
Луис Салливан как-то сказал: «Форма следует за функцией».
Помните, что методы – это глаголы языка программирования, а классы – имена существительные. Не старайтесь делать методы огромными, включающими в себя все на свете. Будет гораздо понятнее, если вы разобьете класс на несколько методов. Так вы не запутаетесь в собственном коде, и другие люди его тоже поймут.
Небольшой пример-визуализация сказанного. Работа со слушателем:
buttonSave.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { buttonLogin(); } });
Просто выносим функцию нашей кнопки в отдельный метод:
protected void buttonLogin() { System.out.println("Вот так будет намного удобнее :)"); }
Комментирование программного кода
Это особенно важно, если вы новичок или пишете действительно большую программу, к которой придется возвращаться снова и снова. Если вы оставите код без единого комментария, рискуете не понять его спустя некоторое время.
Также это большой плюс при приеме на работу. Выполняя ТЗ работодателя, не забывайте комментировать код: так вы показываете свою серьезность и умение писать код, понятный для всех.
Комментировать можно строку или несколько строк, выделяя таким образом часть программного кода. Например, я всегда вывожу размещение элементов в своеобразный блок:
/*** Расположение элементов ***/ panel.setLayout(new GridBagLayout()); GridBagConstraints c = new GridBagConstraints(); c.gridx = 0; c.gridy = 0; panel.add(labelQuestion, c); c.gridx = 0; c.gridy = 1; panel.add(labelSpace, c);
А вот вариант объяснения одной из строк:
FileWriter writer; try { writer = new FileWriter("text.txt", false); writer.write(textName.getText()); writer.flush(); //Финализирует выходное состояние, очищая все буферы вывода } catch (IOException e) { e.printStackTrace(); }
Удобно, правда? Работодатель или напарники по проекту будут того же мнения.
Минимализм vs информативность
Нет, от сокращения имен код не станет проще: это лишь сделает его запутанным и непонятным. Отсутствие комментариев также будет недостатком. Но есть еще ряд приемов, которые заметно уменьшат код. К таким приемам относятся:
- Удаление лишних проверок и условий.
- Избегание дублирования.
- Умение пользоваться конкретным языком программирования.
С первым пунктом вроде все понятно, но давайте проясним. К ненужным проверкам относится в т. ч. проверка на null. Иногда это даже попахивает паранойей. Например:
obj = new Object(); ... if (obj != null) { obj.call(); }
А порой одно и то же условие проверяется несколько раз, словно что-то могло измениться без нашего вмешательства (разумеется, это не относится к коду, с которым действительно происходили соответствующие метаморфозы).
Дублирование – бич начинающих программистов. Из этого пункта вытекает и третий, ведь если человек знает все тонкости языка, он не станет намеренно увеличивать количество строк. Но странно то, что часто такие требования предъявляются именно новичкам, у которых просто недостаточно опыта, чтобы хорошо разобраться во всех тонкостях, например, ООП. Подобное исправляется лишь регулярной практикой.
Рекурсия – не панацея
Многие считают рекурсию лучшим средством для устранения всего лишнего. Если говорить о внешнем виде программного кода, это, несомненно, правда. Пример с факториалом:
public static void factorial() { //итерация int fact = 1; int n = 5; for (int i = 1; i<=n; i++){ fact*=i; System.out.println(fact); } }
static int factorial(int n){ //рекурсия int res; if (n == 1) return 1; res = fact(n-1)*n; return res; }
Да, в этом случае код и там, и там маленький, но если прибавить кучу условий и строк – все изменится. Например, вот рекурсивное решение ханойской башни:
public static void hanoi(int n, String a, String b, String c){ if (n == 1) System.out.println("#" + n + " " + a + " -> " + c); else { hanoi(n - 1, a, c, b); System.out.println("#" + n + " " + a + " -> " + c); hanoi(n - 1, b, a, c); } }
А теперь вообразите, сколько строк выдаст новичок, столкнувшийся с ханойской башней и работающий только с итерацией :)
Но не все так гладко. Попробуйте посчитать факториал большого числа. Вероятно, IDE зависнет, пытаясь переварить ваше решение, и это понятно: рекурсия «кушает» много памяти, так как метод каждый раз вызывает сам себя. Конечно, если в поставленной задаче рекурсия необходима (например, при обработке древовидных структур), использовать ее нужно, но ни в коем случае не злоупотреблять. Компактность-то она обеспечит, но что потом делать с памятью?
Не бойтесь перемен!
Здесь собраны очень простые рекомендации, о которых знает каждый, вот только далеко не каждый их использует. Допустим, вместо «многослойных» if-ов можно использовать оператор (x ? y : z).
Пример с if:
if (x) { var1 = y; } else { var1 = z; }
Пример с (x ? y : z):
var1 = x ? y : z;
Также не забывайте о существовании forEach(), который избавит вас от претензий в стиле «Многа букав»:
public static void forEach(){ int [] mas = {1, 2, 3, 4, 5}; for (int i : mas){ System.out.println(i); } }
Объединяйте вложенные if. Посмотрите, насколько проще становится код.
Было:
if (a) { if (b) { do_something(); } }
Стало:
if (a && b) { do_something(); }
Поговорим о боли под названием «try-catch»
Читаемость кода часто усугубляется повсеместными блоками try-catch, которые сильно портят «картинку». Кроме того, по мере чтения такого программного кода теряются цель и логика происходящего в нем. А все должно быть предельно понятным, особенно для стороннего человека. Правильно обрабатывать возможные ошибки – признак настоящего мастерства.
Да, блоки try-catch напрямую влияют на объем вашего кода. Да, полностью избавиться от этого нельзя, но можно свести к минимуму строки внутри такого блока, вынеся все остальные за его пределы. Но если такое дробление будет подразумевать создание еще большего количества try-catch – лучше обойтись без подобных экспериментов.
Еще исключение может обрабатываться где-нибудь внизу метода, не царапая глаз в середине кода.
Логирование
Не пренебрегайте лог-файлами! Эти «ребята» всегда помогут в создании и сопровождении вашего будущего ПО, ведь на поиски и обработку ошибок будет уходить гораздо меньше времени.
Вспомните ситуации, когда один из элементов приложения не работал или обрабатывал что-то непонятным образом, а что за причина – неизвестно. С лог-файлом время поиска проблемных участков сократится в разы.
Да, такие файлы предназначены для более сложных проектов, и в каких-нибудь Hello-world-программах просто не нужны. Но если вы разрабатываете приложение вдвоем или командой, либо это тестовое задание на допуск к собеседованию – займитесь логированием: интервьюер будет впечатлен, а команда – лишена необходимости переворачивать весь код вверх тормашками в поисках ошибки.
Тестирование
Юнит-тестирование (или модульное тестирование) – еще одна составляющая хорошего программного кода, которая заставит помучиться. Зато в итоге вы сможете запросто проверять на правильность отдельные модули.
Суть в том, что вы пишете тесты для каждой функции, которая не относится к тривиальным и составляет костяк программы. С юнит-тестами можно проверить, к чему приведут последующие изменения в коде.
Используйте бейджики качества кода
Часто в файлах README можно наблюдать небольшие иконки. Что же там делают данные графические элементы? Это бейджики, которые показывают, насколько хорош код. Зачастую используются либо перфекционистами, либо в серьезных проектах, но если даже незначительный по важности код прошел такую проверку – это огромный плюс.
Где получить бейджик? Для этого существуют специальные сервисы. Например, мне нравится Travis CI, предназначенный для сборки и тестирования ПО. Он использует в качестве хостинга GitHub, куда и коммитится проект, так что никаких проблем с привязкой Трэвиса к проекту нет.
Поначалу сложно понять, почему же сервис ругается и не пропускает код, но когда вы поймете, дальнейшее сотрудничество с сервисом пойдет как по маслу.