Прочитав очередные вредные советы про стандарты оформления кода (раз, два, тысячи их), я не смог удержаться, чтобы не поделиться своими измышлениями на эту тему. Долгие годы я вынашивал в своём подсознании чувство «что-то тут не так». И вот, пришло время определиться с тем, что не так, и почему. Рискуя быть закиданным тухлыми бананами, я всё же пишу эту статью тут, а не в своём личном блоге, потому, что это очень важная тема и хочется, чтобы как можно большее число разработчиков поняли её суть и, возможно, пересмотрели свои взгляды на жизнь… кода.
Типичные проблемы многих таких стайл-гайдов:
1. Плохо обоснована их целесообразность
2. Они декларируют конкретные правила, вместо принципов.
3. Эти правила плохо обоснованы и нередко построены на противоречащих принципах.
В упомянутой статье всё обоснование необходимости стандартизации заключается в:
1. [тут располагается картинка про ещё один стандарт] «Стандарт нужен для того, чтобы был стандарт» — не обосновывает его наличие.
2. В любом более-менее крупном проекте всегда будет куча кода, не соответствующая текущим веяниям моды оформления: изменения стайл-гайда со временем, легаси код, код сторонних библиотек, автогенерированный код. Это неизбежно и не так уж и плохо, как на первый взгляд может показаться.
3. То же что и первый пункт.
4. Уже теплее, но опять же, не обосновывается почему продуктивность от этого должна вырасти, и главное — на сколько.
По своему опыту могу сказать, что самое худшее решение — привыкнуть писать и читать код в каком-то одном стиле, от чего любой код написанный «не по правилам» будет вызывать раздражение, тревогу, гнев и желание навязывать окружающим свои привычки. Куда полезнее научиться воспринимать исходники, игнорируя оформление. И для этого наличие разнооформленного кода даже полезно. Я не говорю, что надо расслабиться и писать всё в одну строку, но, чтобы так не делать, есть куда более практичные причины, чем «стандарт нужен, чтобы всё было стандартно».
Как написать грамотный стандарт:
1. Определился с целями
2. Сформулировать принципы и провалидировать их на соответствие целям
3. Сформулировать минимум правил, для реализации этих принципов
Цель: снизить стоимость поддержки путём наложения на себя и команду ограничений.
В чём заключается поддержка:
1. написание нового кода
2. изменение существующего, в том числе и автоматическая
3. поиск нужного участка к��да
4. анализ логики работы кода
5. поиск источника неверного поведения
6. сравнение разных версий одного кода
7. перенос кода между ветками
Какие принципы помогут достичь поставленной цели:
Причина проста: если при изменении одной строки требуется изменение других, то это повышает риск конфликтов при слиянии веток. Каждый конфликт — это дополнительное иногда значительное время на его разрешение.
Несмотря на то, что эта простая идея проскакивает в одном из правил упомянутой в начале статьи, в другом же правиле мы видим явно противоречащую рекомендацию:
Вертикальное выравнивание — это может быть и красиво, но совершенно не практично по следующим причинам:
1. Добавление строки с более длинным именем (например, icon--person-premium) приведёт к изменению всех строк в группе.
2. Автоматическое переименовывание в большинстве случаев собьёт выравнивание (например, при изменении icon--person на icon--user в большинстве инструметов).
3. Иногда пробелы становятся неоправданно длинными, от чего воспринимать код становится сложнее.
Также тут можно заметить лишний семиколон (semicolon, точку с запятой). Единственная причина его появления в этом месте — слепое следование правилам стайл-гайда, не понимая его принципов.
В многострочных правилах, это действительно необходимо, чтобы добавляемые в конец строки не приводили к изменению уже существующих. Например:
Если вы пишете на javascript и можете позволить себе отказаться от ie8, то можете использовать хвостовую пунктуацию и в литералах:
Другой аспект этого принципа заключается в том, чтобы располагать на отдельных строках те сущности, которые меняются как правило независимо. Именно поэтому отдельные css-свойства не стоит располагать в одну строку. Более того, не стоит увлекатьс�� и комплексными свойствами.
Ещё один яркий пример нарушения этого принципа — цепочки вызовов методов:
Тут мы постарались разместить каждое звено на отдельной строке, что позволяет добавлять/удалять/изменять звенья не трогая соседние строки, но между ними всё равно остаётся сильная связь из-за которой мы не можем, например, написать так:
Чтобы добавить такую логику придётся разбить цепочку на две и исправить их начала:
А вот при такой записи мы имеем полную свободу действий:
Если вам кажется, что в коде не хватает так называемых «секций», то скорее всего вы подобрались к верхнему порогу восприятия, когда, уже сложно находить нужные его участки. В этом случае естественным желанием является создание оглавления. Но оглавление в виде комментариев в начале файла не сравнится с оглавлением в виде списка файлов в директории. Располагая код в иерархии файловой системы вы довольно неплохо можете упорядочивать всё прибывающее число сущностей. Примерно то же самое вы скорее всего делаете и в рантайме, располагая код в иерархии неймспейсов, так что нет никакой причины иметь для одной сущности разные пространства имён: в рантайме и в файловой системе. Проще говоря, имена и иерархия директорий должна соответствовать именам и иерархии пространств имён.
Часто можно встретить размещение файлов в нескольких больших корзинах: «все картинки», «все скрипты», «все стили». И по мере роста проекта, в каждой из них появляется иерархия, частично одинаковая, но и с неизбежными отличиями. Задумайтесь: а так ли важен тип файла? Пространства имён куда важнее. Так зачем нужны эти типизированные корзины? Не лучше ли все файлы одного модуля хранить рядом, в одной директории, какими бы ни были их типы? Тем более, что типы могут меняться. Сравните:
Во втором случае, разрабатывая очередную формочку, вам не придётся метаться между директориями, раскладывая файлы в разные места. В случае удаления компоненты, вы не забудете удалить и картинки. Да и переносить компоненты между проектами становится гораздо проще.
В отличие от письменной речи, которая читается строго последовательно, программный код на современных языках программирования представляет из себя двухмерную структуру. В них зачастую нет, например, необходимости ставить точки (семиколоны) в конце предложений:
JS частично понимает двухмерность кода, поэтому в нём семиколоны в конце строк являются тавтологиями:
А вот CSS не понимает, поэтому в нём, без них не обойтись:
Для улучшения восприятия токенов языка, пробелы могут быть расставлены совсем не по правилам письменной речи:
Для более удобной работы с автодополнением, слова могут изменять свой порядок, выстраиваясь от более важных и конкретных к менее важным и общим:
А правило именования коллекций с постфиксом «s» (что в большинстве случаев даёт множественную форму слова) в целях единообразия даёт безграмотные с точки зрения английского языка слова:
Но это меньшее зло по сравнению с требованием от каждого программиста хорошего знания всех английских словоформ.
Поиск по имени — довольно частая операция при работе с незнакомым кодом, поэтому важно писать код так, чтобы по имени можно было легко найти место, где оно определяется. Например, вы открыли страничку и обнаружили там класс «b-user__compact». Вам нужно узнать как он там появился. Поиск по строке «b-user__compact» ничего не выдаёт, потому, что имя этого класса нигде целиком не встречается — оно склеивается из кусочков. А всё потому, что кто-то решил уменьшить копипасту ценой усложения дебага:
Не делайте так. Если склеиваете имя из кусочков, то убедитесь, что эти кусочки содержат полный неймспейс того модуля, где он вводится в употребление:
По полученному классу «my-user__my-block-compact» сразу видно, что он склеен из двух кусков: один определён в модуле «my/block», а другой в «my/user» и оба легко находятся по соответствующим подстрокам. Аналогичная логика возможна и при использовании css-препроцессоров, где мы встречаемся с теми же проблемами:
Если же не используете css-препроцессоры, то тем более:
Продолжать можно было бы долго, но на этом пожалуй закончим. Все проекты разные: где-то нужно быстрое прототипирование, а где-то дол��ая поддержка; где-то используются статически типизированные языки, а где-то динамически. Важно тут одно — прежде чем объявлять какие-то правила единственно верными, сформулируйте цели, принципы, и убедитесь, что правила действительно им соответствуют и отбросьте всё лишнее. Незачем связывать себя по рукам и ногам, боясь оступиться, если достаточно поставить перила в нужных местах.
Стандарты кодирования
Типичные проблемы многих таких стайл-гайдов:
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;
}
Резюме
Продолжать можно было бы долго, но на этом пожалуй закончим. Все проекты разные: где-то нужно быстрое прототипирование, а где-то дол��ая поддержка; где-то используются статически типизированные языки, а где-то динамически. Важно тут одно — прежде чем объявлять какие-то правила единственно верными, сформулируйте цели, принципы, и убедитесь, что правила действительно им соответствуют и отбросьте всё лишнее. Незачем связывать себя по рукам и ногам, боясь оступиться, если достаточно поставить перила в нужных местах.