Когда я был совсем маленьким, я писал код на BASIC. Я бормотал себе под нос заклинания “иф икс больше нуля тхен дыщ-дыщ-дыщ”. Дыщ-дыщ-дыщ — это три пробела. Да, я вбивал их вручную. Да, видимо тогда мне еще не довелось узнать о том, что есть способ проще. И что then читается немного не так. Но зато уже тогда я знал, что содержимое условий и циклов нужно сдвигать! Так делают профи.
С тех пор мой стиль оформления кода успел претерпеть некоторые изменения. Об истории этих самых изменений я и хочу сегодня рассказать.
Но для начала небольшое отступление о структуре статьи. Кроме лирического рассказа о моих собственных поисках, я хотел поделиться информацией, которая может оказаться полезна для других. Поэтому в процессе рассказа я выделю ряд законов форматирования — этаких мета-правил, из которых можно вывести все прочее. В конце рассказа вас ждет отдельный перечень этих законов, для повторения, ну а после него список совсем уж практических рекомендаций в стиле dos and don’ts. Так что, если вы из того поколения, которое не умеет любит читать текст, можете сразу перемотать в конец и смотреть по пунктам.
Смутные времена
После BASIC’а была эпоха Delphi, про которую я мало что помню. Наверное, форматирование не очень волновало меня тогда. Но потом я наконец-то созрел до “настоящего языка” — С++. Заодно пытался освоить ООП и, как и полагается юному любителю видеоигр, написать свой графический движок. И вот тут-то стало по-настоящему жарко! Мало того, что технологии были сложными, так я еще постоянно боролся с форматированием. Фигурную скобочку в конце строки или на новой? А что делать, если у функции слишком длинный список аргументов? А амперсанд/звездочку прижимать к типу, к переменной или отбивать пробелами с обеих сторон? И, главный вопрос: как оформлять “шапки” у классов1?
Может быть, код должен выглядеть как-то так?
Или так?
И я постоянно ловил себя на том, что текущий вариант мне не нравится. Я натыкался на код, в котором выбранный стиль выглядел плохо. И мне приходилось каждый раз думать, как отформатировать код в каждом конкретном случае. В итоге я сам путался, на каком стиле остановился и был непоследователен. И это меня бесконечно бесило — ведь код должен быть оформлен одинаково! Иначе как-то непрофессионально.
Собственно, это мучения подвели меня к первому озарению в вопросах форматирования кода: форматирование должно быть механическим. Это значит, что для форматирования кода не нужно обладать каким-то там чувством прекрасного или утонченным художественным вкусом. Правила форматирования должны быть настолько просты, чтобы их могла выполнять программа.
Разве так важно, чтобы код форматировался программой? Ведь настоящие программисты работают в старых джедайских редакторах и контролируют размещение всех скобочек и запятых непосредственно силой мысли?
Да, важно. Суть не в том, чтобы код форматировался программой. Суть в том, чтобы правила были простыми. Желательно — безусловными. Т.е. списки аргументов всегда выглядят вот так. А объявление класса всегда выглядит вот так. Даже если в данном конкретном случае вам это кажется некрасивым. Форматирование — это не тот вопрос, о котором вам нужно думать, когда вы пишете код. Именно поэтому правила должны быть предельно простыми. Чтобы вы им следовали автоматически. Чтобы вы не решали каждый раз, как же мне разбить список аргументов, который лишь чуть-чуть не влазит в условленные 120 символов.
Немного дисциплины
Спустя небольшое время после прихода к C++, я разжился экземпляром настоящей библии программирования — Совершенным Кодом. Я прочитал эту книгу взахлеб за каких-то неделю или две. До сих пор поражаюсь, как мне раньше удавалось читать такие толстенные книги!
Так вот, в Совершенном Коде был раздел про форматирование2. И в качестве “основной теоремы форматирования” там значилось следующее: форматирование должно отражать структуру программы. Правило, на мой взгляд, отличное. Но с тем, как о нем писал Макконнелл, была одна проблемка: Макконнелл не одобрял фигурную скобку на новой строке. Т.е., по Макконнеллу, хорошее форматирование это так:
for (int i = 0; i < MAX_LINES; i++) { ReadLine(i); ProcessLine(i); }
Или (о боже мой!) так:
for (int i = 0; i < MAX_LINES; i++) { ReadLine(i); ProcessLine(i); }
Но только не так:
for (int i = 0; i < MAX_LINES; i++) { ReadLine(i); ProcessLine(i); }
Что?! Как?! Это же самый православный способ форматировать C++! Когда я впервые читал Совершенный Код, я для себя поместил этот пункт в список спорных — таких, по поводу которых опытные программисты (а я ж тогда был дофига опытным!) могут иметь другое мнение. Кажется, этот пункт был в списке единственным…
И вот теперь, десять лет спустя, с высоты всей своей неопытности заявляю: Макконнелл был прав. Скобочки на новой строке — отстой. В том числе и потому, что не отражают структуру кода. Хотя лично для меня это и не единственная причина. Но об этом позже.
Второе офигенно полезное правило форматирования, которое я вынес из Совершенного Кода: вам не должно требоваться переформатирование при изменении кода. А это значит, что все стили в духе “висячей строки” идут лесом:
Void Clear(Bool nClearColor = true, Bool nClearDepth = true, Bool nClearStencil = true);
И все табличные форматирования идут лесом:
bool isActive() const { return active_; } bool isMouseOver() const { return mouseOver_; } ... DECLARE_SIGNAL( WindowSignal, OnResize ); DECLARE_SIGNAL( WindowMouseSignal, OnMouseMove ); DECLARE_SIGNAL( WindowMouseSignal, OnMouseDown ); DECLARE_SIGNAL( WindowMouseSignal, OnMouseUp );
О, сколько же времени я потратил, ломая голову над тем, нужно ли геттеры форматировать табличкой или “по-правильному”. И ведь частенько форматировал табличкой! Даже зная, что это не правильно. Каюсь, был дурак.
Немного дизайна
А потом в моей жизни случилась Scala. А в ней, как и в Java, было принято писать фигурные скобки в конце строки. И вот тут-то начались новые страдания!
Казалось бы, в чем разница: ну ставь ты ее в конце строки и все. Ан нет! Ведь когда я пишу скобочку на новой строке, то между заголовком и содержимым получается пустая строка. И вы не поверите, как сильно это влияет на остальной код! Как много в нем появляется пустых строк и в других местах! И как упорно после этого начинаешь доказывать, что настоящие профессионалы не скупятся на пустые строки, мол, это помогает читать код. Смысловые блоки выделяет, ага. Вот, поглядите:
void AActor::BeginDestroy() { ULevel* OwnerLevel = GetLevel(); UnregisterAllComponents(); if (OwnerLevel && !OwnerLevel->HasAnyInternalFlags(EInternalObjectFlags::Un... { OwnerLevel->Actors.RemoveSingleSwap(this, false); } Super::BeginDestroy(); }
У вас не возникает желания поставить пустую строку перед if’ом в этом коде? Ведь в противном случае получается, что заголовок условия связан с предшествующим блоком сильнее, чем с телом. И я сейчас говорю на полном серьезе. По-моему, это одно из ключевых правил дизайна: размещать связанные вещи рядом, а несвязанные — поодаль. И меня, честно, бесит код, который не следуют этому правилу. Так что, знаете, я включу это правило в список ключевых законов оформления кода.
Казалось бы, если ставить фигурную скобку в конце строки, все становится чудесно: теперь заголовки будут прижаты к содержимому и все будут счастливы. Но не тут-то было! Ведь правило про скобочку в конце строки применимо не только к условиям и циклам, но еще и к функциям. А что, если у меня в функции несколько логических блоков?
Видите, я тут специально поставил пустую строку в самом начале функции. А иначе получится, что первый блок (val sourceBounds = …) имеет какое-то особое отношение к списку аргументов.
С другой стороны, когда я делаю функции такими разреженными, становится сложнее искать границы между ними. Тем более, что некоторые (большинство?) функций совсем крошечные и в них никаких пустых строк не требуется. Вот такими метаниями и сопровождалось мое программирование на Scala: то уплотнял, то снова разрежал функции. Никак этот вопрос не давал мне покоя.
И вот теперь самое время отступить немного назад и сказать, что чуть ранее Scala в моей жизни также случился Стив Йегги.
Стив Йегги — чудесный писатель, который подарил мне много приятных часов. А на тему оформления кода у него есть, как обычно, длинная и, как обычно, увлекательная статья — Portrait of a Noob. Когда я первый раз ее прочитал, то, безусловно, покивал: ха-ха, нубы! Ну, конечно, приятно не считать себя таковым. А кто считает? Но спустя какое-то время я перечитал ее, и таки обратил внимание на один из посылов статьи: профессионалы пишут плотный код. И я тут же решил, что хочу быть профессионалом. И начал писать плотный код:
Иногда эти методы начинали мне казаться слишком страшными и непонятными, бесструктурными, но я держался. Вы, наверное, можете возразить, что методы не должны быть большими. Что всегда можно выделить еще один метод. И будете чертовски правы! Но ведь этот код используется только в одном месте… Но ведь тут вычисление кэшируется, а если вынести функции, будет 2 раза вычисляться… Возможно все эти отмазки вы тоже регулярно слышите. Или говорите. Я лично бывал в обоих лагерях. Заспойлерю: правы те, кто за выделение методов.
И немного смысла
И это приводит нас к последнему, и, пожалуй, самому главному закону форматирования кода: пишите программы, а не текст! В программе у вас есть функции, классы, перечисления и всякие прочие штуки, которые поддерживает ваш язык программирования. Делайте программы из них. Не из пустых строк, комментариев и выравнивания.
На самом деле, этот последний закон не имеет отношения к форматированию. С точки зрения форматирования, это просто переформулировка старого доброго “мусор на входе — мусор на выходе”. Если у вас на входе плохо структурированная программа, то на выходе будет нечитаемая херня, независимо от того, насколько творчески вы подойдете к форматированию.
И, знаете, если вы начнете работать над структурой программы, а не над текстом и будете стремится довести ее до совершества, произойдет чудо. Окажется, что пустые строки для “отделения смысловых блоков” не нужны. Табличное форматирование не нужно. Комментарии практически не нужны. И ваши функции станут маленькими и плотными3:
Не нравится ФП? Это вы зря. Но, вот, пожалуйста:
Единственное, что слегка раздражает, так это вереница закрывающих скобок по одной на строку. Но тут разве что Python спасет. Или LISP. Лучше LISP.
Законы оформления кода
А вот и обещанный свод законов. Здесь я отсортировал их в порядке важности, а не в порядке появления в тексте:
- На первом месте структура, оформление — вторично
- Форматирование должно отражать структуру программы
- Оформление не должно мешать изменению программы
- Оформление должно следовать правилу близости
- Оформление должно быть механическим
Особо внимательный читатель мог заметить, что правило, которое я привел в тексте первым, попало в самый конец списка, а правило, которое я привел последним — в начало. Мне это показалось занятным: похоже, самые глубокие и важные вещи мы понимаем в последнюю очередь.
Практические советы
Правильно форматируйте списки
Под списком я имею в виду любую конструкцию вашего ЯП, в которой перечисляются элементы одного ранга: список параметров функции или типа, полей класса, аргументов в вызове и даже (о боже мой!) — операндов операций.
Так вот, существует только 2 правильных способа отформатировать список:
- все аргументы в одну строку
- по одному аргументу на строку
Пожалуйста, никогда не смешивайте эти стили. Пожалуйста, никогда не вставляйте разрыв строки в рандомном месте тупо для того, чтобы строка влезла в экран:
var floorMap = PlacementMap.MakeFloorMap(_plane, target, angle, walls, obstacles);
Такое форматирование не соответствует структуре программы, а также нарушает правило близости: obstacles находится гораздо ближе к floorMap, чем к другим параметрам и к самой функции.
Далее, никогда не выравнивайте код как обычный текст с простым переносом строк по ширине:
bool UGameplay::MakeWeaponLineTrace(FHitResult& OutHit, const AActor* WeaponActor, const FVector &LineStart, const FVector &ShootDirection, float FireDist const FName &TraceTag, ECollisionChannel TraceChannel) { ... }
Такой код не отражает структуру программы, а еще в нем очень сложно находить отдельные аргументы. Но, пожалуйста, не пытайтесь решить эту проблему группировкой параметров по “логическим блокам”:
bool UAssetUtils::SpawnAllAssetsFromDirectory(UObject* WorldContextObject, const FString &Path, const TArray &PathsToExclude, UClass* FilteringClass, FVector PlacementLocation, float PlacementDistance, float SketchHeight) { ... }
В примере выше имеется в виду, что PlacementLocation и PlacementDistance каким-то образом более связаны друг с другом, чем, скажем, Path и FilteringClass. Все это может быть и разумно, но вспомните пятый закон: оформление должно быть механическим. Компьютер не обязан вникать в тонкости вашей семантики, чтобы отформатировать код. И вам тоже не следует тратить мысленные усилия на то, чтобы решить, какие параметры в какие группы стоит объединить.
Поэтому, когда параметры перестают умещаться на одну строку, поступайте тупо и скучно — размещайте по одному параметру на строке:
public PlayerPlaceAllowance PlacementAllowance( PlayerDoll doll, Vector3 where, Vector3 up, out IEnterable destEnterable) { RaycastHit hit; if (Physics.Linecast(castFrom, castTo, out hit, (int)LayerMask.Blocks)) { ... } }
Вам может показаться, что тратить по одной строке на параметр — слишком расточительно. Но вспомните: форматирование должно отражать структуру программы. Форматирование не виновато, что у вас есть куча методов с большим количеством параметров. Так что, вместо того, чтобы прятать плохую структуру красивым форматированием, лучше исправьте структуру. А форматирование оставьте компьютеру.
Правильно форматируйте выражения
На самом деле это просто расширение предыдущего совета. Но я решил выделить это в отдельный пункт, дабы заострить внимание. Потому что больно уж часто я сталкиваюсь с плохо отформатированными выражениями. А выражения — это чуть ли не лучшее, что случилось с программированием после ассемблера.
Ключ к форматированию выражений очень прост: любое выражение можно представить в виде списка. Например, 2 + 2 — это список из двух элементов, а x || y || z — список из трех элементов. Если мы заменим инфиксную нотацию на префиксную, то получим + 2 2 и || x y z. А чтобы можно было делать вложенные выражения, добавим скобки вокруг: (+ 2 2) и (|| x y z). Поздравляю, вы получили диалект LISP’а.
Так вот, LISP — это ключ к форматированию выражений. Этот язык помогает понять структуру выражения. А когда четко понимаешь структуру, не возникает сомнений, как ее следует показать4.
Итак, давайте перейдем к примерам. Немного булевой логики:
return mappings == null || (actualForRules != null && !actualForRules.SequenceEqual(rules));
Мой любимый тернарный оператор (он же if expression):
var replacement = defaultReplacementShader ? new Material(defaultReplacementShader) : null;
Если вам это кажется дикостью, не спешите. На самом деле это образцовое форматирование, поскольку оно предельно четко отражает структуру выражения: в корне дерева у нас присвоение, в которое вложен один элемент — оператор выбора, в который вложено 2 дочерних элемента — истинная и ложная ветки соответсвенно:
assignment condition true-clause false-clause
Ну и, напоследок, ультимативный пример, от которого у вас даже может засвербить в известном месте:
const auto Attachment = Turrets::PredictTurretAttachment( GetOwner(), TurretClass ? Inflate( // to be sure that server will not reject our placement ... TurretClass.GetDefaultObject()->GetRelativeSmashSphere(), GetOwnerRole() < ROLE_Authority ? 10.f : 0.f) : FSphere(ForceInit), bAttachesToAllies, bAttachesToEnemies, StartPoint, EndPoint);
Не пишите комментарии, пишите код
Возможно, вам доставляет эстетическое удовольствие подобный код:
void AActor::GetAttachedActors(TArray<class AActor*>& OutActors) const { OutActors.Reset(); if (RootComponent != nullptr) { // Current set of components to check TInlineComponentArray<USceneComponent*> CompsToCheck; // Set of all components we have checked TInlineComponentArray<USceneComponent*> CheckedComps; CompsToCheck.Push(RootComponent); // While still work left to do while(CompsToCheck.Num() > 0) { // Get the next off the queue const bool bAllowShrinking = false; USceneComponent* SceneComp = CompsToCheck.Pop(bAllowShrinking); // Add it to the 'checked' set, should not already be there! if (!CheckedComps.Contains(SceneComp)) { CheckedComps.Add(SceneComp); AActor* CompOwner = SceneComp->GetOwner(); if (CompOwner != nullptr) { ... } } } } }
Возможно, вы чувствуете себя настоящим профессионалом, когда перемежаете свой код комментариями. Но лично я думаю, что вы застряли в профессиональном развитии, если продолжаете писать так.
Не группируйте элементы по смыслу
Если у вас есть поле и парочка методов, работающая с ним, не делайте так:
class AMySuperActor { ... void ProcessUpDownInput(float DeltaPitch); float AccumulatedPitch; void ProcessLeftRightInput(float DeltaYaw); float AccumulatedYaw; };
Делайте по-скучному — типы, методы, поля:
class AMySuperActor { ... void ProcessUpDownInputImpl(float DeltaPitch); void ProcessLeftRightInputImpl(float DeltaYaw); float AccumulatedPitch; float AccumulatedYaw; };
Вам может показаться, что этот совет противоречит самому главному правилу: делайте структуру, а не форматирование. Мол, ведь логически эти функции и поле связаны! Вот именно — логически, но не структурно. Если хотите их связать структурно — выделите класс. Не хотите? Ну тогда, пожалуйста, не захламляйте код своим авторским стилем: мне все равно придется долго и мучительно продираться сквозь ваше творение, чтобы понять, из каких кусков оно состоит и что тут с чем связано.
То же самое касается и перемежения разных категорий доступа. Старый добрый порядок public-protected-private работает отлично, не надо изобретать велосипед.
Не тратьте место
Если вы сомневаетесь, как лучше форматировать те или иные конструкции (а это уже хорошо!), отдавайте предпочтение более компактному стилю. Может быть, вы как раз думаете, где лучше ставить закрывающую скобку у списка аргументов функции: в конце строки или на новой. Ставьте в конце строки:
PreviewActor = GetWorld()->SpawnActorDeferred<APreview>( PreviewActorClass, Attachment.Transform, GetOwner());
Возможно вам доставляет некоторое эстетическое неудовольствие ассиметричность подобного стиля. Возможно, вам хотелось бы сделать так:
PreviewActor = GetWorld()->SpawnActorDeferred<APreview>( PreviewActorClass, Attachment.Transform, GetOwner() );
Но мы тут не про эстетику, а про структуру и прагматизм. В случае, когда у вас будут вложенные выражения (а я надеюсь, они у вас будут), компактный стиль сработает лучше. Сравните:
var result = function( arg1, subFunction( subArg1, subSubFunction( subSubArg1, subSubArg2)), arg3);
против:
var result = function( arg1, subFunction( subArg1, subSubFunction( subSubArg1, subSubArg2 ) ), arg3);
На мой взгляд, пустота перед arg3 только отвлекает.
Заключение
Ну вот, пожалуй, и все. Прикладных советов получилось меньше, чем я ожидал. Ровно столько же, сколько и “законов” — непорядок! Ну, что поделаешь. Я хотел обратить внимание на наиболее раздражающие моменты, с которыми часто сталкиваюсь. И оказалось, что в оформлении кода таких раздражителей не так уж и много. Гораздо чаще проблемы бывают в структуре программы, а не в оформлении. Но это тема для отдельного разговора.
Что же касается моей истории в целом, мораль крайне проста: я идиот и потратил кучу времени на то, чтобы прийти к вполне очевидным и логичным выводам. Не повторяйте мои ошибки, будьте разумны. Вы же программисты.
Примечания
- Заспойлерю на случай, если после прочтения статьи это все же останется неочевидным: “шапки” не нужны!
- На самом деле в Совершенном Коде есть разделы практически про все: про именование, про то, как писать методы и классы, как отлаживаться, как, в конце концов, думать про программирование. Так что, если вы не читали, настоятельно советую. Да, издание довольно старое и некоторые вещи лично я бы сегодня советовал делать по-другому. Но все же, пользы от прочтения этой книги будет несоизмеримо больше, чем вреда.
- Приведенные примеры кода не являются примерами “совершенства”. Это просто неплохо структурированный код. Но, черт возьми, хотел бы я, чтобы весь софт состоял хотя бы из такого кода!
- Поинтересуйтесь, как обстоят дела с форматированием в самом LISP’е. Возможно вы удивитесь, узнав, что этот вопрос был решен более 50 лет назад, и с тех пор LISP-программисты не тратят свое время на глупости.
> У вас не возникает желания поставить пустую строку перед if’ом в этом коде?
блин! да! пора подумать про стиль написания..
> типы, методы, поля:
порядок важен? я вот люблю в обратном порядке
Если честно, я на работе встречаю так много сопротивления описанным идеям, что даже не знаю, всерьез ты это или троллишь :-) Но если считать, что всерьез: была у меня когда-то идея более умного форматирования для IDE, при которой интервалы между строками будут зависеть от семантики. Видим заголовок новой функции? Подвинем немного от предыдущего кода. Заголовок класса? Подвинем чуть больше. И никакого ручного управления пустыми строками. На самом деле, я бы в идеале вообще хотел, чтобы программисты не имели возможности форматировать код или чтобы эта возможность была сведена к минимуму.
Имхо не важен. На самом деле я тоже придерживаюсь обратного порядка, если это не C++. А вот в C++ сложилась привычка помещать типы в начало, т.к. в противном случае тупо не скомпилится.
> всерьез ты это или троллишь
я серьезно, просто с годами, работая над своими проектами, я выработал свой собственный стиль, и он весьма размашистый, потом он скорректировался на работе, но компактнее он не стал, и я почему-то воспринимал его как правильный, ибо так легко читать, но это сравнимо с книгой в которой слова написаны огромным шрифтом, читать то просто но блин, листать нужно часто.. Твоя статья натолкнула на размышления о альтернативах, что нужно не просто выработать стиль который просто читать из-за размашистости, но нужно двигаться в обратную сторону – читать просто из-за компактности! Я когда-то делал несколько контрибьютов в линукс, и тогда я не понимал как такой плотный код можно легко понимать, но проблема понимания крылась в другом – я просто был еще не опытный программист. А теперь вот вспоминаю и понимаю, что с кодом все было хорошо, и даже очень хорошо. Так что спасибо за статью, очень полезно для размышлений да и для совершенствования.
> идея более умного форматирования для IDE
класс, весьма интересно. На сколько я понял, ты просто пишешь код а ИДЕ сама форматирует.. написал сигнатуру метода, энтер, а дальше ИДЕ добавила сама пустую строку, так? Если да то это клево! Платформы на которых строятся современные ИДЕ, те же Intellij или VS позволяют писать весьма продвинутые плагины, не думал что-то такое сделать?
а вот и атоматизация – https://github.com/spring-io/spring-javaformat
Классно, спасибо за ответ! Рад слышать, что статья натолкнула на какие-то размышления.
Что касается автоформатирования, кажется подобные штуки набирают популярность: dart style, go fmt. Я лично ничего против не имею, скорее наоборот — наверное очень это полезно, особенно при работе в команде. Но это не меняет принципиально то, как мы работаем с программой: в виде текста. Мы редактируем ее как текст, храним как текст, diff’аем как текст, на вход компилятору она подается как текст. При этом на каждом шаге было куда эффективней работать с программой не как с текстом. Представь, что все наши инструменты внезапно стали работать с AST вместо текста. IDE нам просто не дает ввести некомпилируемый код. Система контроля версий вместо “2 строки добавилось, 1 удалилась” говорит “метод был переименован”, а компилятор не тратит постоянно время на синтаксический разбор. Вот такая у меня идея периодически крутится в голове. Да, я понимаю, что это немного утопия и вполне вероятно, что существуют объективные причины, почему наши инструменты выглядят не так. Но мне было бы интересно выяснить это на практике. Возможно я когда-нибудь созрею до того, чтобы начать свой маленький домашний проектик на эту тему.
Да, плагины для IDE — это куда более жизнеспособная и рабочая идея. Но на данный момент меня сейчас больше интересует исследование возможностей, а не практический результат. Поэтому в сторону разработки чего-то реального для реально используемых IDE я пока что не смотрю.
В любом случае спасибо за статью, это очень полезно встряхнуть мозг и поставить его в “некомфортные” условия (тут скорее просто непривычные).
Я тут могу провести аналогию с тем как я осваивал когда-то event sourcing, когда твое представление о привычной модели данных должно сильно измениться. Я поначалу не понимал, зачем и все было как-то “неудобно” использовать. А потом проникся и теперь доволен. Пусть я не использую это изо дня в день, но зато я расширил кругозор.