Так ли важно внутренне устройство кода или результат важнее?
Думаю, никто не будет спорить, что программный код должен быть чистым. Все хорошо знают, что цена ошибки в продакшне возрастает в десятки, а то и в сотни раз.
Давайте определимся. Что же является вашим продуктом? Код или полезное действие, который он обеспечивает? Если вы делаете open-source библиотеку, то сам код и является продуктом. В этом случае, несомненно, важно, как он устроен внутри. Но в большинстве случаев пользователи никак не взаимодействуют с кодом, и поэтому его внутреннее устройство, казалось бы, не имеет значения. Но всё ли так просто?
Проблема в том, что посредственное отношение к коду практически неизбежно влияет на качество самого продукта и его будущее развитие. Любое изменение проекта чревато поломкой хрупкой конструкции из хаков и патчей, где глюки и тормоза в этом случае образуются лавинообразно.
Начнем с базовых вещей.
1.Тщательно выбираем названия функций, переменных и классов.
Название должно показывать намерение. Выбор хороших названий требует времени, но в итоге сохраняет его вам в будущем.
Если поле называется “name”, там должно храниться именно название объекта, а не дата его создания или порядковый номер в массиве и таких примеров множество.
Несмотря на очевидность этого правила, программисты его нарушают. Думаю, не стоит пояснять, каким образом это сказывается на читабельности кода.
Чтобы избежать таких проблем, необходимо максимально тщательно продумывать название каждой переменной, каждого метода и класса. При этом надо стараться не только корректно, но и, по возможности, максимально полно охарактеризовать назначение каждой конструкции
Очевидно, так же стоит минимизировать использование переменных с названиями i, j, k, s. Переменные с такими названиями могут быть только локальными и иметь только самую общепринятую семантику. В случае i, j, это могут счетчики циклов или индексы в массиве. Хотя, по возможности, и от таких счетчиков стоит избавляться в пользу циклов foreach и функциональных выражений.
Переменные же с названиями ii, i1, i_help и т.д., не стоит использовать никогда..
2.Называй одинаковые вещи одинаково, разные по-разному.
Не в буквальном смысле, но одна из самых обескураживающих трудностей, возникающих при чтении кода — это употребляемые в нем синонимы и омонимы. Иногда в ситуации, когда две разных сущности названы одинаково, приходится тратить по несколько часов, чтобы разделить все случаи их использования Без такого разделения нормальный анализ, а следовательно и осмысленная модификация кода, невозможны в принципе.
Примерно то же самое можно сказать и об “обратной” проблеме — когда для одной и той же сущности/операции/алгоритма используется несколько разных имен.
Вывод простой: в своих программах к возникновению синонимов и омонимов надо относиться крайне внимательно и всеми силами стараться подобного избегать.
3.Предпочитаем прямое-косвенному.
Любые явно выписанные алгоритмы или условия уже являются самодокументированными, т. к. их назначение и принцип действия уже описаны ими же самими. И наоборот, любые косвенные условия и операции сильно затрудняют понимание сути происходящего. Звучит непонятно, но давайте разберем на примере:
Очевидно, что прямое условие в данном случае гораздо читабельнее и требует лишнего времени на осознание происходящего.
4.Все объекты объявляем в максимально узкой области видимости.
*Но не стоит забывать, что если создание объекта через
сreateSomeObj() — дорогая операция, внесение ее в цикл может неприятно сказаться на производительности программы, даже если читаемость от этого и улучшится.*
5.Комментарии не исправят плохой код
Многие считают, что хорошо размещенный комментарий может быть очень полезен, поэтому хотелось бы коротко пояснить, чем же так плохи комментарии:
A. В хорошо написанном коде они не нужны.
B. Если поддержка кода часто становится трудоемкой задачей, поддержкой комментариев обычно просто никто не занимается и через некоторое время комментарии устаревают и начинают обманывать людей.
C. Они засоряют собой пространство, тем самым ухудшая внешний вид кода и затрудняя читаемость.
D. В подавляющем большинстве случаев они пишутся лично Капитаном Очевидностью.
Ну а нужны комментарии в случаях, когда написать код хорошо по тем или иным причинам не представляется возможным. Т.е. писать их надо для пояснения участков кода, без них трудночитаемых. К сожалению, в реальной жизни такое встречается регулярно, хотя расценивать это следует лишь как неизбежный компромисс.
6.Оставляем в коде только то, что действительно используется.
“Висящие” функции, которые никем нигде не вызываются; участки кода, которые никогда не выполняются; целые классы, которые нигде не используются, но их забыли удалить — уверен, каждый мог наблюдать такие вещи в своем проекте, и может быть, даже воспринимал их, как должное.
На деле же, любой код, в том числе неиспользуемый, требует плату за свое содержание в виде потраченного внимания и времени.
Поскольку неиспользуемый код реально не выполняется и не тестируется, в нем могут содержатся некорректные вызовы тех или иных процедур, ненужные или неправильные проверки, обращения к процедурам и внешним библиотекам, которые больше ни для чего не нужны, и множество других сбивающих с толку или просто вредных вещей.
Поговорим о более технических вещах.
Для поддержки достойного уровня читаемости и управляемости необходимо, чтобы код был:
A. максимально линейным;
B. коротким;
1.Линеаризация кода.
Линейность является самым неочевидным пунктом, и именно ей чаще всего пренебрегают.
Наверное, потому что за годы учебы (и, возможно, научной деятельности) мы привыкли обсуждать от природы нелинейные алгоритмы с оценками типа O(n3), O(nlogn) и т.д.
Это все, конечно, хорошо и важно, но, говоря о реализации в реальных проектах, обычно приходится иметь дело с алгоритмами совсем другого свойства.
С линейностью связывают не столько асимптотическую сложность алгоритма, сколько максимальное количество вложенных друг в друга блоков кода, либо же уровень вложенности максимально длинного подучастка кода.
2.Необходимо научиться выделять основную ветку алгоритма.
Разберем на примере клиент заказчик. Есть некий алгоритм.
Клиент сообщает пожелания→Мастер осматривает и говорит стоимость→Поиск дефектов→Составляем заказ на запчасти→Берем предоплату, обозначаем срок→Клиент уезжает.
Уровень вложенности значительно уменьшился. Код стал более читабельным и логичным.
3.Используем break, continue, return или throw, чтобы избавиться от блока else.
Разумеется, неверным был бы вывод, что вообще никогда не нужно использовать оператор else. Во-первых, не всегда контекст позволяет поставить break, continue, return или throw. Во-вторых, выигрыш от этого может быть не столь очевиден, как в примере выше, и простой else будет выглядеть гораздо проще и понятней, чем что-либо еще.
Ну и в-третьих, существуют определенные издержки при использовании множественных return в процедурах и функциях.
Поэтому эта (и любая другая) техника должна восприниматься как подсказка, а не как безусловная инструкция к действию.
4.Выносим сложные подсценарии в отдельные процедуры / методы.
В случае “алгоритма ремонта” мы довольно удачно выбрали основную ветку, то альтернативные ветки у нас все остались весьма короткими.
*Обратите внимание, что правильно выбрав имя выделенной процедуре, мы, кроме того, сразу же повышаем самодокументированность кода. Теперь для данного фрагмента в общих чертах должно быть понятно, что он делает и зачем нужен.*
5.Помещаем в try...catch только то, что необходимо.
Надо отметить, что блоки try...catch вообще являются болью, когда речь идет о читаемости кода, т.к. часто, накладываясь друг на друга, они сильно повышают общий уровень вложенности даже для простых алгоритмов.
Бороться с этим лучше всего, минимизируя размер участка внутри блока. Т.е. все строки, не предполагающие появление исключения, должны быть вынесены за пределы блока. Хотя в некоторых случаях с точки зрения читаемости более выигрышным может оказаться и строго противоположный подход: вместо того, чтобы писать множество мелких блоков try..catch, лучше объединить их в один большой.
6.Объединяем вложенные операторы if.
Тут все очевидно.
Поговорим о не менее интересной области, такой как минимизация кода.
Думаю, было бы лишним пояснять, что уменьшая количество кода, используемого для реализации заданного функционала, мы делаем код гораздо более читаемым и надежным.
В этом смысле, идеальное инженерное решение — это, когда ничего не сделано, но все работает, как требуется. Разумеется, в реальном мире крайне редко доступны идеальные решения, и поэтому у нас, программистов, пока еще есть работа.
1.Устраняем дублирование кода.
Разумеется, самым очевидным методом борьбы с проблемой является вынесение переиспользуемого кода в отдельные процедуры и классы.
При этом всегда возникает проблема выделения общего из частного. Зачастую даже не всегда понятно, чего больше у похожих кусков кода: сходства или различий. Выбор тут делается исключительно по ситуации. Тем не менее, наличие одинаковых участков размером в пол-экрана сразу говорит о том, что данный код можно и нужно записать существенно короче
Например:
Или же обратный вариант:
2.Избегаем написания “велосипедов”. Используем по максимуму готовые решения.
Велосипеды — это почти как копипаст: все знают, что это плохо, и все регулярно их пишут. Можно лишь посоветовать хотя бы пытаться бороться с этим злом.
Большую часть времени перед нами встают задачи или подзадачи, которые уже множество раз были решены, будь то сортировка или поиск по массиву. Общее правило заключается в том, что стандартные задачи имеют стандартное решение, и это решение дает нам возможность получить нужный результат, написав минимум своего кода.
Порой есть соблазн, вместо того, чтобы что-то искать, пробовать и подгонять, быстренько набросать на коленке свой велосипед. Иногда это может быть оправдано, но если речь идет о поддерживаемом в долгосрочной перспективе коде, минутная “экономия” может обернуться часами отладки и исправления ошибок.