Идеальный код
Мы часто слышим об идеальном (или совершенном) коде. Однако, что это? Кто-нибудь видел его в реальной жизни? Можно ли вообще описать требования к такому коду? Я давно думал о написании пособия, где хотел бы рассмотреть эти вопросы, но только несколько лет опыта преподавания на курсах Java и общения со студентами дали мне необходимый посыл и материалы для создания такой книги. Здесь я буду в основном рассматривать вопросы кодирования, однако аналогично можно рассмотреть идеальный дизайн или архитектуру. Данная статья является более короткой версии моей книги.
В виду того, что совершенство — щекотливая тема, я не утверждаю, что мои рассуждения являются абсолютной истиной, а цель статьи — убедить всех в их правильности. Однако они основаны на многолетнем опыте в ИТ, здравом смысле и навыках программирования. Поэтому, даже если мои рассуждения не убедят вас, они, по крайней мере, заставят вас задуматься и приведут к собственным рассуждениям. А это уже неплохо.
Правила игры
Я уже предчувствую вопросы об идеальности кода. Разве можно сделать что-то идеальное, тем более в такой постоянно меняющейся среде как ИТ? Кто будет оценивать степень идеальности? Я согласен, что наша индустрия постоянно меняет правила, появляются новые языки и платформы, развивается синтаксис языков. Не меняются лишь разработчики и потребители их продуктов — пользователи.
Я думаю, что вы встречали немало книг, где тема идеального кода разбирается изнутри — с точки зрения программирования. Но мне кажется, мало объяснить, что такое идеальный код. Разработчики должны понимать, почему важно писать идеальный код. Поэтому, для того, чтобы дать более целостный и аргументированный ответ, я предлагаю копнуть глубже и вспомнить, что написание кода — это лишь часть процесса под названием разработка ПО. Давайте взглянем на разработку продукта с точки зрения всех задействованных участников. Главная мысль — совершенный код является частью совершенного продукта. Иначе говоря, совершенный код помогает выпустить совершенный продукт.
Что отличает совершенный продукт? Качество. Здесь под качеством прежде всего понимают стабильность продукта, количество ошибок и их критичность, а также эффективность продукта (объем потребляемых ресурсов и скорость работы). Под стабильностью понимают отсутствие ошибок и сбоев в течение всего время использования продукта, а также качество поддержки — оперативность исправления критических ошибок и добавления фич, которые нужны пользователям.
Для меня одним из примеров такого качественного продукта является космический аппарат «Вояджер», который с 1977 года бороздит просторы Солнечной системы, работает без сбоев и глюков, усердно передавая информацию на Землю.
Теперь взглянем на программный продукт глазами разработчика. Основные задачи, которые он выполняет в ходе работы над проектом:
— Чтение кода;
— Модификация кода;
— Запуск и отладка.
Чем быстрее он будет выполнять эти задачи, чем меньше будет побочных эффектов, тем быстрее он сможет выпустить очередную версию продукта, тем динамичнее будет развиваться его карьера. Таким образом, если обобщить все показатели, которые приведены в этой главе, то к проекту (а, значит и коду) следует предъявить следующие требования:
— Легко читать и разбирать;
— Легко изменять (исправлять);
— Быстро запускать (и перезапускать) на разных ОС;
— Можно переиспользовать и тестировать;
— Использует бесплатную платформу и библиотеки;
— Минимально допустимое количество ошибок (или полное отсутствие);
— Минимальное потребление ресурсов;
— Стабильность при изменении конфигурации/окружения;
— Безопасность.
Если проанализировать этот список требований, то можно прийти к выводу, что наш идеальный код должен быть:
— Читабельный (readable);
— Очевидный (obvious);
— Компактный (precise);
— Само-объясняющийся (self-describing);
— Современный (modern);
— Гибкий (flexible);
— Расширяемый (extensible);
— Эффективный (effective);
— Масштабируемый (scalable);
— Безопасный или надежный (safe);
— Защищенный (secured);
— Кроссплатформенный (cross-platform);
— Бесплатный (free).
Понятно, что невозможно достичь всех характеристик сразу и возникает вопрос о приоритете. Что важнее? Чего легче достичь? Чем можно пренебречь?
Поэтому я для себя разбил все характеристики на группы, исходя из их важности для проекта и достижимости разработчиками. На первом месте оказалась группа, содержащая читабельность, компактность и само-объясняющийся код. О них мы и поговорим в этой статье.
Читабельный код
Почему мы начали с читабельности? Разработчики тратят большую часть времени (иногда до 90%) на чтение кода. Учитывая, что разработка — это командная работа, чаще всего они просматривают именно чужой код. Это может быть и код проекта, и код сторонней библиотеки, и код JDK.
Почти каждый разработчик может сказать о коде, читабельный он или нет, только взглянув на него. С моей точки зрения, читабельный код — это код, который можно одинаково легко понять, независимо от того, какой объем этого кода вы будете рассматривать.
Насколько читабельность интуитивна или ее можно достичь, если следовать некоторым правилам и конвенциям? Есть несколько рекомендаций:
- Сложные методы должны быть короткими, а простые могут быть длинными;
- Избегайте вложенных циклов и принудительного выхода из цикла, где это возможно;
- Избегайте вложенных try/catch и анонимных классов;
- Старайтесь писать каждое выражение на отдельной строке;
- Избегайте чрезмерного количества методов, вызываемых в цепочке один за одним;
- Используйте Java конвенции.
Фактически читабельность основана на объеме информации, который должен держать в голове разработчик, чтобы понимать любую строку вашего кода. Тут многое зависит и от опыта, поэтому я всегда советую новичкам читать больше чужого кода. Читать и анализировать.
Возьмем для примера данный код. Он хоть и понятен, но тяжел для восприятия:
private static final long COUNTER = 126684343434343l; private static final String KEY_DATA = "OURINTERNALKEYVALUE";
Если же мы его немного изменим, то код сразу прибавит в читабельности:
private static final long COUNTER = 126_684_343_434_343l; private static final String KEY_DATA = "OUR_INTERNAL_KEY_VALUE";
Компактность
С компактностью вроде все просто. Чем меньше кода, тем меньше читать и тем меньше ошибок можно сделать. Но где грань, за которой компактность переходит в нечитабельность и ухудшает гибкость?
С моей точки зрения, компактный код — это код, который нельзя уменьшить в объеме, не навредив остальным характеристикам.
Исходя из моего опыта, я бы разделил все случаи нарушения компактности на три группы:
- Незначительные (minor) случаи;
- Дублирование кода или нарушение принципов ООП;
- Использование языковых конструкции или API в контексте, который не для этого предназначен.
Таким образом, компактность или развернутость кода зависит от знаний и навыков разработчика (как языка программирования, так и стороннего API). Чем больше и глубже разработчик знает API, тем лучше он его может использовать. Это кстати приводит к интересному выводу: делая свой код переиспользуемым, вы помогаете делать его компактным.
Коллекции — одна из самых интересных тем, и если вы с ней хорошо знакомы, то можете значительно упростить код. Например, как создать список из одного элемента. Почти всегда делают вот так:
public static List<String> toList(String item) { List<String> items= new ArrayList<>(); items.add(item); return items; }
Хотя более изящным вариантом будет
public static List<String> toList(String item) { return Collections.singletonList(item); }
И напоследок вернемся ко строкам. Я думаю, что вы достаточно часто видели такой код:
public static String replace(String text) { return text.replaceAll("a", " "). replaceAll("b", " "). replaceAll("c", " "); }
Код достаточно понятный, однако в случае большого числа замен еще и громоздкий. Не все знают, что метод replaceAll принимает первым параметром не просто строку, а еще и регулярное выражение. Поэтому более компактная форма записи:
public static String replace(String text) { return text.replaceAll("[abc]", " "); }
Она еще и работает намного быстрее.
Само-объясняющийся код
Я думаю, что среди читателей это книги есть две большие группы людей. Одни считают, что комментарии в коде не нужны. Другие, наоборот, считают, что комментарии обязательно нужны всегда и везде. Каково мое мнение?
Давайте посмотрим на следующий код:
class Info { private String cronExpression = "0 0 0/2 1/1 * ? * "; }
Что хранит поле cronExpression
? Не каждый, даже знакомый с CRON выражениями, может абсолютно точно и уверенно сказать об этом. Сторонники второго подхода выбрали бы такой подход. Мы напишем комментарий, который опишет эту строку.
class Info { /** * This is expression to run process every two hours */ private String cronExpression = "0 0 0/2 1/1 * ? * "; }
Теперь стало более понятно. Но что если у нас такие выражения встречаются в других местах проекта? Нам придется в каждом из них писать этот комментарий. Кроме того, если кто-то поменяет значение cronExpression
, поменяет ли он комментарий? Все зависит от его порядочности и наличия свободного времени. Не стоит забывать, что написание комментариев требует времени.
Как бы поступил сторонник первого подхода? Он бы добавил константу, на которую можно ссылаться в любом месте проекта:
class Info { private static final String TWO_HOURS_DELAY = "0 0 0/2 1/1 * ? * "; private String cronExpression = TWO_HOURS_DELAY; }
Казалось бы, спор разрешился. Выиграли противники комментариев. Мы указали, что есть некоторый процесс, который будет запускаться каждые два часа. Но теперь у разработчиков, просматривающих код, может возникнуть закономерный вопрос. А почему мы используем именно такой интервал? И вот здесь без комментариев не обойтись:
class Info { private static final String TWO_HOURS_DELAY = "0 0 0/2 1/1 * ? * "; /** * We use two-hour delay as it's maximum time * to finish the previous job */ private String cronExpression = TWO_HOURS_DELAY; }
Заключение
Я думаю, что во время прочтения этой статьи вам не раз приходила мысль о целесообразности всех этих принципов. Стоит ли учить шаблоны, стандарты и методологии, если можно просто писать работающий код? Главное ведь в коде — выполнение требуемых задач.
Такая точка зрения может быть оправдана, если для разработчиков главное — сделать проект и забыть о нем. Тогда в результате работы команды получается нечто, похожее на черный ящик. Все знают, что он работает, но никто не знает, как. Более того, этот черный ящик монолитен, и он настолько разросся, что его нельзя вынести из его комнаты. Приходится либо увеличивать комнату, либо резать ящик по живому. В обоих случаях результат предсказуем.
Совершенный код — не догма и не истина. Он не вечен и меняется вместе с теми правилами, которые диктует отрасль. В этой статье я хотел показать, что такой код — это не столько результат работы гения-программиста, сколько следование определённым требованиям и стандартам ИТ-индустрии.