Прочитав очередные вредные советы про стандарты оформления кода (раз, два, тысячи их), я не смог удержаться, чтобы не поделиться своими измышлениями на эту тему. Долгие годы я вынашивал в своём подсознании чувство «что-то тут не так». И вот, пришло время определиться с тем, что не так, и почему. Рискуя быть закиданным тухлыми бананами, я всё же пишу эту статью тут, а не в своём личном блоге, потому, что это очень важная тема и хочется, чтобы как можно большее число разработчиков поняли её суть и, возможно, пересмотрели свои взгляды на жизнь… кода.
Стандарты кодирования
Типичные проблемы многих таких стайл-гайдов:
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: -16px 0 ; }
.icon--files { background-position: 0 -16px; }
.icon--settings { background-position: -16px -16px; }
Вертикальное выравнивание — это может быть и красиво, но совершенно не практично по следующим причинам:
1. Добавление строки с более длинным именем (например, icon--person-premium) приведёт к изменению всех строк в группе.
2. Автоматическое переименовывание в большинстве случаев собьёт выравнивание (например, при изменении icon--person на icon--user в большинстве инструметов).
3. Иногда пробелы становятся неоправданно длинными, от чего воспринимать код становится сложнее.
Также тут можно заметить лишний семиколон (semicolon, точку с запятой). Единственная причина его появления в этом месте — слепое следование правилам стайл-гайда, не понимая его принципов.
В многострочных правилах, это действительно необходимо, чтобы добавляемые в конец строки не приводили к изменению уже существующих. Например:
.icon {
display: inline-block;
width: 16px;
height: 16px
}
.icon {
display: inline-block;
width: 16px;
height: 16px; /* добавили семиколон */
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) 10px 0 black;
}
.icon {
background: url(/img/sprite.svg) 10px 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: -16px -16px; }
.icon--settings { background-position: -16px -16px }
JS частично понимает двухмерность кода, поэтому в нём семиколоны в конце строк являются тавтологиями:
function run() {
setup();
test();
teardown();
}
function run() {
setup()
test()
teardown()
}
А вот CSS не понимает, поэтому в нём, без них не обойтись:
.icon {
display: inline-block;
width: 16px;
height: 16px;
}
Для улучшения восприятия токенов языка, пробелы могут быть расставлены совсем не по правилам письменной речи:
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;
}
Резюме
Продолжать можно было бы долго, но на этом пожалуй закончим. Все проекты разные: где-то нужно быстрое прототипирование, а где-то долгая поддержка; где-то используются статически типизированные языки, а где-то динамически. Важно тут одно — прежде чем объявлять какие-то правила единственно верными, сформулируйте цели, принципы, и убедитесь, что правила действительно им соответствуют и отбросьте всё лишнее. Незачем связывать себя по рукам и ногам, боясь оступиться, если достаточно поставить перила в нужных местах.
Автор: vintage