Принципы написания кода
Прочитав очередные вредные советы про стандарты оформления кода (раз, два, тысячи их), я не смог удержаться, чтобы не поделиться своими измышлениями на эту тему. Долгие годы я вынашивал в своём подсознании чувство «что-то тут не так». И вот, пришло время определиться с тем, что не так, и почему. Рискуя быть закиданным тухлыми бананами, я всё же пишу эту статью тут, а не в своём личном блоге, потому, что это очень важная тема и хочется, чтобы как можно большее число разработчиков поняли её суть и, возможно, пересмотрели свои взгляды на жизнь… кода.Стандарты кодированияТипичные проблемы многих таких стайл-гайдов:1. Плохо обоснована их целесообразность2. Они декларируют конкретные правила, вместо принципов. 3. Эти правила плохо обоснованы и нередко построены на противоречащих принципах.В упомянутой статье всё обоснование необходимости стандартизации заключается в:
Хорошее руководство по оформлению кода позволит добиться следующего:1. Установление стандарта качества кода для всех исходников;2. Обеспечение согласованности между исходниками;3. Следование стандартам всеми разработчиками;4. Увеличение продуктивности.
1. [тут располагается картинка про ещё один стандарт] «Стандарт нужен для того, чтобы был стандарт» — не обосновывает его наличие.2. В любом более-менее крупном проекте всегда будет куча кода не соответствующая текущим веяниям моды оформления: изменения стайл-гайда со временем, легаси код, код сторонних библиотек, автогенерированный код. Это неизбежно и не так уж и плохо, как на первый взгляд может показаться.3. То же что и первый пункт.4. Уже теплее, но опять же, не обосновывается почему продуктивность от этого должна вырасти, и главное — на сколько.По своему опыту могу сказать, что самое худшее решение — привыкнуть писать и читать код в каком-то одном стиле, от чего любой код написанный «не по правилам» будет вызывать раздражение, тревогу, гнев и желание навязывать окружающим свои привычки. Куда полезнее научиться воспринимать исходники игнорируя оформление. И для этого наличие разнооформленного кода даже полезно. Я не говорю, что надо расслабиться и писать всё в одну строку, но, чтобы так не делать, есть куда более практичные причины, чем «стандарт нужен, чтобы всё было стандартно».
Как написать грамотный стандарт:1. Определился с целями2. Сформулировать принципы и провалидировать их на соответствие целям3. Сформулировать минимум правил, для реализации этих принципов
Итак, попробуем Цель: снизить стоимость поддержки путём наложения на себя и команду ограничений.В чём заключается поддержка:1. написание нового кода2. изменение существующего, в том числе и автоматическая3. поиск нужного участка кода4. анализ логики работы кода5. поиск источника неверного поведения6. сравнение разных версий одного кода7. перенос кода между ветками
Какие принципы помогут достичь поставленной цели:
1. Строки файла должны быть максимально независимы. Причина проста: если при изменении одной строки требуется изменение других, то это повышает риск конфликтов при слиянии веток. Каждый конфликт — это дополнительное иногда значительное время на его разрешение.Не смотря на то, что эта простая идея проскакивает в одном из правил упомянутой в начале статьи, в другом же правиле мы видим явно противоречащую рекомендацию:
.icon--home { background-position: 0 0; } .icon--person { background-position: -16 px 0; } .icon--files { background-position: 0 -16 px; } .icon--settings { background-position: -16 px -16 px; } Вертикальное выравнивание — это может быть и красиво, но совершенно не практично по следующим причинам:1. Добавление строки с более длинным именем (например, icon--person-premium) приведёт к изменению всех строк в группе.2. Автоматическое переименовывание в большинстве случаев собьёт выравнивание (например, при изменении icon--person на icon--user в большинстве инструметов).3. Иногда пробелы становятся неоправданно длинными, от чего воспринимать код становится сложнее.Также тут можно заметить лишний семиколон (точку с запятой). Единственная причина его появления в этом месте — слепое следование правилам стайл-гайда, не понимая его принципов.В многострочных правилах, это действительно необходимо, чтобы добавляемые в конец строки не приводили к изменению уже существующих. Например:
.icon { display: inline-block; width: 16 px; height: 16 px } .icon { display: inline-block; width: 16 px; height: 16 px; // добавили семиколон background-image: url (/img/sprite.svg) // полезное изменение } Если вы пишете на javascript и можете позволить себе отказаться от ie8, то можете использовать хвостовую пунктуацию и в литералах: var MainThroubles = [ 'fools', 'roads', 'fools on roads', ] var GodEnum = { father: 0, son: 1, holySpirit: 2, } Другой аспект этого принципа заключается в том, чтобы располагать на отдельных строках те сущности, которые меняются как правило независимо. Именно поэтому отдельные css-свойства не стоит располагать в одну строку. Более того, не стоит увлекаться и комплексными свойствами. .icon { background: url (/img/sprite.svg) 10 px 0 black; } .icon { background: url (/img/sprite.svg) 10 px 0; /* смещения в спрайте жестко связаны с самим спрайтом */ background-color: black; /* фоновый цвет меняется независимо от картинки */ } Ещё один яркий пример нарушения этого принципа — цепочки вызовов методов: messageProto .clone () .text (text) .appendTo (document.body) .fadeIn () Тут мы постарались разместить каждое звено на отдельной строке, что позволяет добавлять/удалять/изменять звенья не трогая соседние строки, но между ними всё равно остаётся сильная связь из-за которой мы не можем, например, написать так: messageProto .clone () .text (text) if (onTop){ .appendTo (document.body) } else { .prependTo (document.body) } .fadeIn () Чтобы добавить такую логику придётся разбить цепочку на две и исправить их начала: var message = messageProto .clone () .text (text) if (onTop){ message.appendTo (document.body) } else { message.prependTo (document.body) } message.fadeIn () А вот при такой записи мы имеем полную свободу действий: var message = messageProto.clone () message.text (text) message.appendTo (document.body) message.fadeIn () var message = messageProto.clone () message.text (text) if (onTop){ message.appendTo (document.body) } else { message.prependTo (document.body) } message.fadeIn () 2. Не сваливать все яйца (код) в одну корзину (файл/директорию). Если вам кажется, что в коде не хватает так называемых «секций», то скорее всего вы подобрались к верхнему порогу восприятия, когда, уже сложно находить нужные его участки. В этом случае естественным желанием является создание оглавления. Но оглавление в виде комментариев в начале файла не сравнится с оглавлением в виде списка файлов в директории. Располагая код в иерархии файловой системы вы довольно не плохо можете упорядочивать всё прибывающее число сущностей. Примерно то же самое вы скорее всего делаете и в рантайме, располагая код в иерархии неймспейсов, так что нет никакой причины иметь для одной сущности разные пространства имён: в рантайме и в файловой системе. Проще говоря, имена и иерархия директорий должна соответствовать именам и иерархии пространств имён.Часто можно встретить размещение файлов в нескольких больших корзинах: «все картинки», «все скрипты», «все стили». И по мере роста проекта, в каждой из них появляется иерархия, частично одинаковая, но и с неизбежными отличиями. Задумайтесь:, а так ли важен тип файла? Пространства имён куда важнее. Так зачем нужны эти типизированные корзины? Не лучше ли все файлы одного модуля хранить рядом, в одной директории, какими бы ни были их типы? Тем более, что типы могут меняться. Сравните:
img/ header/ logo.jpg menu_normal.png menu_hover.png main/ title-bullet.png css/ header/ logo.css menu.css main/ typo.css title.css js/ menu.js spoiler.js tests/ menu.js spoiler.js header/ logo/ header-logo.jpg header-logo.css menu/ menu.css menu.js menu.test.js menu_normal.png menu_hover.png main/ title/ title.css title-bullet.png spoiler/ spoiler.js spoiler.test.js Во втором случае, разрабатывая очередную формочку, вам не придётся метаться между директориями, раскладывая файлы в разные места. В случае удаления компоненты вы не забудете удалить и картинки. Да и переносить компоненты между проектами становится гораздо проще.3. Язык программирования — принципиально не естественный язык. В отличие от письменной речи, которая читается строго последовательно, программный код на современных языках программирования представляет из себя двухмерную структуру. В них зачастую нет, например, необходимости ставить точки (семиколоны) в конце предложений: .icon--settings { background-position: -16 px -16 px; } .icon--settings { background-position: -16 px -16 px } JS частично понимает двухмерность кода, поэтому в нём семиколоны в конце строк являются тавтологиями: function run () { setup (); test (); teardown (); } function run () { setup () test () teardown () } А вот CSS не понимает, поэтому в нём, без них не обойтись: .icon { display: inline-block; width: 16 px; height: 16 px; } Для улучшения восприятия токенов языка, пробелы могут быть расставлены совсем не по правилам письменной речи: say ({message: concat («hello», worldName.get ())}) say ({ message: concat («hello» , worldName.get ()) }) say (document.body, { message: concat («hello» , worldName.get ()) }) Для более удобной работы с автодополнением, слова могут изменять свой порядок, выстраиваясь от более важных и конкретных к менее важным и общим: view.getTopOffset () view.offsetTop_get () view.offset.top.get () А правило именования коллекций с постфиксом «s» (что в большинстве случаев даёт множественную форму слова) в целях единообразия даёт безграмотные с точки зрения английского языка слова: for (man of mans) man.say ('Hello, Mary!') Но это меньшее зло по сравнению с требованием от каждого программиста хорошего знания всех английских словоформ.5. Полные и одинаковые имена одной сущности в разных местах Поиск по имени — довольно частая операция при работе с незнакомым кодом, поэтому важно писать код так, чтобы по имени можно было легко найти место, где оно определяется. Например, вы открыли страничку и обнаружили там класс «b-user__compact». Вам нужно узнать как он там появился. Поиск по строке «b-user__compact» ничего не выдаёт, потому, что имя этого класса нигде целиком не встречается — оно склеивается из кусочков. А всё потому, что кто-то решил уменьшить копипасту ценой усложения дебага: //my/block/block.js My.Block.prototype.getSkinModClass = function (){ return 'b-' + this.blockId + '__' + this.skinId } My.Block.prototype.skinId = 'compact'
//my/user/user.js My.User.prototype.blockId = 'user' Не делайте так. Если склеиваете имя из кусочков, то убедитесь, что эти кусочки содержат полный неймспейс того модуля, где он вводится в употребление: //my/block/block.js My.Block.prototype.getSkinModClass = function (){ return this.blockId + '__' + this.skinId }
My.Block.prototype.skinId = 'my-block-compact'
//my/user/user.js My.User.prototype.blockId = 'my-user' По полученному классу «my-user__my-block-compact» сразу видно, что он склеен из двух кусков: один определён в модуле «my/block», а другой в «my/user» и оба легко находятся по соответствующим подстрокам. Аналогичная логика возможна и при использовании css-препроцессоров, где мы встречаемся с теми же проблемами: //my/block/block.styl .b-block { &__compact { zoom: .75; } }
//my/user/user.styl .b-user { @extends .b-block; color: red; } //my/block/block.styl .my-block { &__my-block-compact { zoom: .75; } }
//my/user/user.styl .my-user { @extends .my-block; color: red; } Если же не используете css-препроцессоры, то тем более: //my/block/block.css .m-compact { zoom: .75; }
//my/user/user.css .b-user { color: red; } //my/block/block.css .my-block-compact { zoom: .75; }
//my/user/user.css .my-user { color: red; } Резюме Продолжать можно было бы долго, но на этом пожалуй закончим. Все проекты разные: где-то нужно быстрое прототипирование, а где-то долгая поддержка; где-то используются статически типизированные языки, а где-то динамически. Важно тут одно — прежде чем объявлять какие-то правила единственно верными, сформулируйте цели, принципы, и убедитесь, что правила действительно им соответствуют и отбросьте всё лишнее. Незачем связывать себя по рукам и ногам, боясь оступиться, если достаточно поставить перила в нужных местах.