За годы присутствия на хабре я прочитал немало статей на тему того, как должен выглядеть идеальный код. И поменьше статьей о том, как конкретно достигать этого идеала. Также стоит отметить, что весьма значительная часть всех этих материалов была переводом западных источников, что, вероятно, является следствием более зрелой отрасли IT «за рубежом», со всеми вытекающими вопросами и проблемами.
К сожалению, во многих случаях авторы либо забираются в недосягаемые выси многослойных архитектур, что требуется в лучшем случае для 1% проектов, либо ограничиваются общими фразами вроде «код должен быть понятен» или «используйте ООП и паттерны», не опускаясь до подробных объяснений, в чем например измеряется «понятность» кода.
В этой статье я хочу попытаться систематизировать те критерии оценки качества кода, и те практики его написания, которые мне удавалось применять в реальных проектах практически независимо от их размеров и специфики, используемого языка программирования и других факторов.
1. Простота
Здесь все уже придумано до нас — существует замечательный принцип KISS, а также афоризм Альберта Эйнштейна «Все должно быть изложено так просто, как только возможно, но не проще», которыми всем нам стоит руководствоваться.
Простой код лучше сложного по всем параметрам — проще для понимания, легче поддается изменениям и тестированию, требует меньше комментариев и документации, содержит меньше ошибок (хотя бы чисто статистически, если мы принимаем среднее количество ошибок по отношению к количеству языковых и логических конструкций в коде за некоторую постоянную величину для каждого конкретного программиста).
Также, перефразируя положение ТРИЗ об идеальной системе можно сказать, что идеальный код — тот, которого нет, причем задача, которую он должен решать — успешно решается. Очевидно, что этот идеал (как и любой другой), недостижим на практике, однако способен направить ум разработчика в нужном направлении.
На практике все эти красивые фразы означают одно — не используйте инструменты, реальная (а не гипотетическая) польза применения которых не превышает ущерб от усложнения и запутывания кода. Не стройте приложение вокруг целого фреймворка ради одной узкой задачи, которую он может помочь решить. Не используйте сложные паттерны там, где можно обойтись простым классом. Не используйте класс для пары функций, которые даже не работают со свойствами родного объекта в своем коде (в обратном случае — используйте). Не оборачивайте в функцию код из 3-х строк, который нигде более в приложении не используется повторно (и не будет использоваться в ближайшем будущем, а не через 150 лет).
Разумеется, все это вовсе не означает, что нужно писать спагетти-код в императивном стиле на 5000 строк. Как только возникнет такое желение, нужно еще раз перечитать и осознать вторую часть цитаты приведенной выше, о том что код должен быть "простым, насколько возможно, но не проще".
Не нужно бояться простых конструкций, если они позволяют решить поставленную задачу. Также не стоит прикрываться размышлениями на тему того, как ваш код может быть теоретически использован при развитии проекта или смене пожеланий заказчика через год-другой. Дело даже не в том, что ваш код будет «в любом случае выброшен и переписан с нуля», как заявляют многие сторонники простой и быстрой разработки. Возможно, что не будет. Но проблема в том, что с высокой долей вероятности вы просто не угадаете нужного направления. Вы спроектируете молоток так, чтобы им можно было (после небольшой доработки напильником) закручивать саморезы, а окажется, что нужно прикрутить автоматическую подачу гвоздей и лазерное наведение при замахе. И ваша архитектура, рассчитанная под развитие в определенную сторону, только проиграет по сравнению с «просто молотком». Поверьте, я знаю, о чем говорю, т.к. уже 5 лет поддерживаю проект в рунете, который вырос и стал довольно успешным вообще не на том, куда были вложены максимальные усилия и ради чего он создавался, а за счет небольшой ветки «побочного» функционала.
2. Концептуальность
Этот критерий во многом пересекается с «простотой» кода, но все-таки я решил его вынести в отдельный раздел. Суть подхода — в использовании концепций, причем в идеале — общепринятых концепций, которые уже широко применяются в других решениях.
Поясню на простой аналогии, что я имею ввиду:
Представьте, что перед вами поставлена задача — заучить результаты умножения всех чисел от 2 до 9 друг на друга. Вы можете просто выписать в строку: 2х2=4, 2х3=6, ..., 3х7=21,… и т.д., после чего приступить к зазубриванию этого текста.
Формально — этим можно пользоваться, и это даже можно выучить. Но есть значительно более практичный вариант — использовать концепцию «таблицы» — т.е. структуры данных, определяющей списки значений X и Y, с возможностью быстро определить точки их пересечения и значение этих точек по заранее заданной формуле (в данном случае Z = X * Y). В результате мы получим известную всем с детства таблицу умножения, которая помимо банальной экономии количества символов в тексте обладает рядом других преимуществ:
Структурное восприятие — помимо самих результатов умножения, мозг получает массу другой полезной информации — о том, что для каждого числа существует одинаковое количество результатов, включая умножение на самого себя, что шаг увеличения произведения при движении вниз по столбцу равняется числу в заголовке этого столбца (и аналогично для строк), и т.д.
Легкость масштабирования — имея на руках таблицу и понимая ее концепцию, нет ничего проще чем масштабировать ее для, например, чисел от 1 до 20. При использовании простого текста время на его доработку увеличивается в разы (я бы даже сказал в квадрат раз).
Легкость модифицирования — также очень легко превратить таблицу умножения в таблицу сложения, либо деления, либо в какую-то другую. Используемая базовая концепция позволяет легко варьировать свойства продукта в определенных пределах без изменения архитектуры.
Обратите внимание, что нам не приходится прикладывать каких-либо дополнительных усилий чтобы таблица «работала» таким образом, а также не нужно прилагать «инструкцию по пользованию таблицами» к нашему документу — в этой «бесплатности» и состоит основная сила использования концепций в программировании. Решения, в которых грамотно используются концепции, мы часто называем изящными.
Примером блестящей концепции является «перенаправление» в UNIX-системах, которое позволяет создавать различные программы, не тратя ни строчки кода на то, как они будут работать с чтением/записью в файлы или другие потоки, либо на взаимодействие с другими программами, например текстовым фильтром grep. Все что нужно — обеспечить стандартный интерфейс ввода/вывода текста.
Таким образом даже несколько десятков наиболее популярных команд порождают тысячи вариантов их использования. Напоминает что-нибудь? Правильно — это решение задачи с помощью «кода, которого нет».
Более простой пример из практики — однажды мне потребовалось ротировать на одном рекламном месте сайта объявления сторонней рекламной сети с двух различных аккаунтов, с условием примерно равного распределения прибыли между ними (с допустимым отклонением 1-2%). Громоздкое решение, которое можно было бы применить «в лоб», состояло в том, чтобы фиксировать в базе данных показы объявлений по каждому аккаунту с учетом стоимости этих показов (которые тоже могли отличаться), и на основании этих данных при каждом следующем открытии страницы принимать решение о том, чьи объявление показывать, чтобы обеспечить минимальный дисбаланс.
Чуть позже выяснилось, что согласно закону больших чисел банальная проверка вероятностей по принципу «орел-решка» при выборе аккаунта полностью решает задачу в рамках заданных условий. Более того, не составляет никаких проблем таким образом распределять показы между 3 и более аккаунтами. Само собой, это не придуманная человеком концепция, а базовый математический принцип, но с точки зрения кода важно лишь одно — это работает.
Принцип «Don't repeat yourself» (сокр. DRY) говорит нам о том, что каждая функциональная единица системы, будь то логический блок кода, функция или целый класс, должна быть представлена в коде только один раз.
Повторяющиеся блоки кода обычно выносятся в функции. Для функций используются подключаемые библиотеки и пространства имен. Когда же дело доходит до классов, в бой вступает целый зоопарк методик, направленных на снижение дублирования кода, начиная от банального наследования и заканчивая сложными системами из «фабрик по производству фабрик» вперемешку с «фасадными адаптерами», которые зачастую требуют больше кода и умственных усилий программиста, чем позволяют сэкономить.
Стремление с самого начала спроектировать код так, чтобы избежать любых повторений — заведомо губительно, т.к. для 80% ситуаций вы не будете знать о дублировании кода, пока не напишите его.
Реальный совет для практического использования в любом проекте — решайте проблему дублирования кода сразу после ее обнаружения (но не раньше!), и делайте это наиболее простым и очевидным способом, доступным вам в текущей ситуации.
Есть несколько очень похожих блоков кода из 5-10 строк, отличающихся лишь парой условий и переменных, но вы пока не уверены, как лучше завернуть их в функцию? Используйте циклы.
Не знаете, в какой класс/объект положить код нового метода? Просто создайте функцию и положите ее в подключаемую библиотеку до тех времен, когда у вас появится более полная информация о разрабатываемой системе.
Используете наследование классов, но приходится полностью переопределять 50-строчный метод в наследнике ради изменения одной логической конструкции? В первую очередь подумайте о разделении большого метода на несколько более изолированных, либо об использовании инстансов других классов со всеми их методами в качестве значений свойств исходного объекта.
Простые и проверенные решения позволят решить 95% всех проблем с дублированием кода, а для оставшихся 5% надо 10 раз подумать о целесообразности механического применения сложных конструкций «из учебников» по сравнению с осмысленным перепроектированием «плохого» участка кода (с упором на снижение сложности и повышение читабельности).
Вы всегда сможете заменить простое решение на более высокоуровневое, и сделать это будет намного проще, чем применять сложный рефакторинг к сырому коду на более позднем этапе, когда дубликаты уже разбросаны по всей системе.
4. Удобочитаемость
Используйте названия переменных, функций, классов и методов, наиболее точно описывающие суть того, что они именуют. Избегайте односимвольных переменных (кроме, разве что, счетчиков цикла типа i, n, k, и то не всегда), и названий типа var1, function2 и т.д. Не бросайтесь и в другую крайность — названия типа CreateTextDocumentWithAttachedFilesViaHTTP — тоже не очень повышают удобство чтения. Оптимальная длина названия: 1-3 слова.
Старайтесь быть последовательными в именовании функций и методов, а также структуры их аргументов и возвращаемых значений. Не повторяйте ошибки разработчиков языка PHP (как пример — имена функций для работы с массивами — array_filter, preg_grep, array_unique, in_array, sort, current т.д.)
Не используйте «магические числа». Исключение только одно — вы на 150% уверены, что никто и никогда не увидит этот код кроме вас, а сами вы никогда не приметесь за его поддержку спустя пару-тройку лет, мучительно вспоминая, что же такое «574».
Не экономьте на пробелах и переносах строк при отделении языковых конструкций друг от друга. Помните, что в литературном английском языке (как и в русском), принято отделять текст после запятых пробелом, а абзацы разделять пустой строкой. Те же самые правила подойдут и программному коду.
Используйте наиболее лаконичные из доступных стандартных конструкций языка. Например, тернарный оператор вместо if-else для блока кода, помещающегося в одну строчку. Не используйте операторы нестандартным, неизвестным основной массе разработчиков образом, если только ваш код не готовится для конкурса.
Комментируйте неочевидные участки кода. Не комментируйте очевидные. Для тренировки внутреннего «определителя очевидности» здорово помогает раз в месяц показывать код коллегам или хотя бы просто знакомым программистам.
Разумеется, само по себе хорошее оформление кода еще не гарантирует качество программы и легкость ее восприятия. Но именно такое внимание к мелочам и отличает настоящего профессионала от простого ремесленника, работающего в стиле «авось сгодится».
Что можно сказать в заключение
Статья получилась несколько более объемной, чем было запланировано, хотя я сознательно избегал обсуждения множества аспектов, вроде связности и связанности, шаблонов проектирования и других понятий, о которых уже написаны целые книги, и можно написать немало новых.
Моей целью было подробно изложить самые основы «правильного» (с моей точки зрения) программирования, многие из которых достойны пера Капитана Очевидности, и тем не менее регулярно игнорируются большинством разработчиков, особенно в веб-сегменте. Хотя соблюдение только этих простых основ, без углубления в дебри перфекционизма, способно сделать их код лучше в разы, во всех отношениях.
Если я что-то упустил или где-то ошибся — конструктивная критика в комментариях всячески приветствуется.