Чтобы от программного кода не отмахивались собеседующие или коллеги, он должен быть удобоваримым. Как написать «конфетку», которая понравится всем?
Роберт Мартин когда-то сказал, что единственным допустимым измерением качества кода является «Что за…».
Позвольте объяснить. Всякий раз, когда на глаза попадается какой-либо код, возникает одна из трех эмоций:
«Что за…», произнесенное с отвращением – этот код вообще не нужен.
«Что за…», выражающее восхищение – «Эй, а ведь этот парень действительно крут!»
«Что за…» с раздражением – в представленном коде невозможно разобраться.
Так что отвечает за реакцию на код? Это чистота кода. Писать чистый и предельно понятный код – признак настоящего профессионала. Как этого добиться?
Такое мастерство создано из двух главных составляющих: знания и работы. Знание учит вас шаблонам, принципам, практике и эвристике, которые необходимы для постоянного совершенствования. Но это знание должно быть подтверждено практикой, без которой оно попросту не будет закрепляться.
Так что красота программного кода – это не так уж просто, и если собеседующему что-то не понравилось, не расстраивайтесь: помните, что это все придет с опытом. В этой статье мы собрали десять принципов по-настоящему хорошего кода.
«Могу ли я узнать ваше имя?»
В программировании имена повсюду. Мы называем функции, классы, аргументы, пакеты и т. д. Иногда мы думаем что-то вроде: «И так понятно, что text – это текстовое поле. Зачем мудрить?» Однако взглянув на код или часть программного кода через неделю, две или месяц, натыкаемся на абракадабру, в которой непонятно что непонятно за что отвечает.
Допустим, вы написали небольшую графическую часть на Java. Что можно сказать о происходящем из представленного ниже программного кода?
setTitle(text1);
setSize(550,360);
setVisible(true);
text2.setEditable(false);
try {
List 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 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, куда и коммитится проект, так что никаких проблем с привязкой Трэвиса к проекту нет.
Поначалу сложно понять, почему же сервис ругается и не пропускает код, но когда вы поймете, дальнейшее сотрудничество с сервисом пойдет как по маслу.