Выбор стиля. Оптимизация визуального представления текста программы

    eyecode


    Грубо говоря, очевидно, что некоторые виды форматирования лучше других.
    — Стив Макконелл, Совершенный код.


    Эта статья об особенностях человеческого зрения и о том, как знание этих особенностей может помочь нам улучшить объективную удобочитаемость наших программ.


    Содержание




    Вступление


    Рискуя вызвать резкое неудовольствие со стороны моих коллег, я применяю принципы, в которые верю с какой-то безрассудной односторонностью. Частично я это делаю в силу того, что в отдельных случаях полезно изложить свою точку зрения с голой простотой и тем самым предоставить дальнейшую шлифовку своей концепции последующим ударам и контрударам критики.
    — Рудольф Арнхейм, Искусство и визуальное восприятие.


    Компьютерный язык — это не просто способ заставить компьютер производить вычисления, а новое формальное средство выражения методологических идей. Таким образом, программы должны писаться для того, чтобы их читали люди, и лишь во вторую очередь для выполнения машиной.
    — Харольд Абельсон, Джеральд Д. Сассман, Структура и интерпретация компьютерных программ.


    Соотношение времени чтения и написания кода превышает 10:1. … Из-за столь высокого соотношения наш код должен легко читаться, даже если это затрудняет его написание.
    — Роберт Мартин, Чистый код: создание, анализ и рефакторинг.


    Наверно, никому не надо доказывать то, что легкость восприятия (удобочитаемость) текста программы является одним из решающих факторов, определяющих успешность её сопровождения и развития.


    Обычно, когда оценивают текст программы с точки зрения ее легкости восприятия, используют термин удобочитаемость. Строго говоря, это не совсем одно и то же, поскольку, как будет показано далее, процесс восприятия программы это больше, чем просто чтение. Тем не менее, поскольку речь идет о тексте, и термин удобочитаемость можно считать достаточно устоявшимся, я также буду использовать его в этом смысле.


    Для поддержания удобочитаемости кода в процессе разработки программы обычно договариваются о некотором общем своде правил форматирования (стиле) исходного кода. Уже само по себе наличие свода таких правил способно оказать положительное влияние на его удобочитаемость и качество, так как, во-первых, формирует у программистов определенные привычки относительно тех языковых конструкций, которые они ожидают увидеть в тексте программы, а, во-вторых, заставляет их внимательнее относиться к тому, что они написали (если, конечно, форматирование кода не переложено полностью на средства автоформатирования).


    Тем не менее, отдельные правила часто вызывают вопросы, поскольку критерии их выбора неясны, и они нередко противоречат аналогичным правилам в других подобных стилях.


    Правила задают конкретные детали оформления кода в целях поддержания удобочитаемости, но при этом нет объяснения того, как эти правила помогают ее достичь. Без понимания этого, решение инженерной задачи формирования удобочитаемого, то есть оптимального с точки зрения легкости восприятия текста программы, подменяется бездумным следованием формальным и часто произвольно выбранным правилам, которые к тому же меняются при переходе с проекта на проект, с языка на язык. В результате формируется ложное представление о том, что сами по себе правила не так важны, и выбор того или иного стиля это всего лишь дело вкуса и привычки.


    Действительно, наши привычки во многом определяют, насколько комфортно мы чувствуем себя в той или иной ситуации и, в частности, воспринимаем тот ли иной стиль форматирования. Но ощущение комфорта вследствие привычки не может быть мерой того, насколько объективно хорош этот стиль: очевидно, что привычка к какому-то стилю может лишь означать, что мы просто перестали замечать специфические особенности этого стиля, которые на деле могут быть контрпродуктивными в смысле формирования объективно удобочитаемого кода.


    Говоря об объективной удобочитаемости, я подразумеваю, что полная удобочитаемость текста состоит из субъективной составляющей, вызванной выработанными привычками и навыками, и о которой мы говорили выше, и объективной. Эта вторая составляющая определяется возможностями и ограничениями общих для всех нормальных в психическом и физическом состоянии людей механизмами восприятия и обработки визуальной информации.


    Таким образом, субъективная составляющая связана с некоторыми частными привычками, которые могут быть изменены, а объективная — с общими психофизическими особенностями зрения человека, которые мы не предполагаем возможным изменять. Поэтому, говоря об оптимизации текста программы, имеет смысл говорить лишь об объективной составляющей удобочитаемости, и поэтому далее в этой статье термин удобочитаемость всегда будет относиться к этой её составляющей.


    Давайте посмотрим подробнее, на то, что нам известно о механизмах восприятия человеком зрительной информации, чтении текстов вообще и чтении и восприятии текстов программ.



    Особенности зрения человека


    Поле зрения


    Поле зрения человека достаточно велико: 55° вверх, 60° вниз, 90° наружу (то есть суммарное поле зрения двумя глазами — 180°) и 60° внутрь. Однако внутри этого поля острота зрения и цветовосприятие распределены неравномерно: острота зрения порядка 1' достигается в области фовеа, формирующей ~2° центрального (фовеального) зрения, но она не так хороша в парафовеальной области (которая покрывает 5° в обе стороны от точки фиксации) и еще хуже на периферии.¹


    Подобным же образом от центра к краю падает и способность различать цвета, причем это изменение различно для разных цветовых компонент. Можно сказать, что, двигаясь от центра человеческой сетчатки к периферии, мы как бы оказываемся на более ранних этапах эволюции, переходя от наиболее высоко организованных структур к примитивному глазу, который различает лишь простое движение теней.


    Рисунок 1. Поле зрения правого глаза человека. Оранжевое пятно — место проекции слепого пятна глазного дна. (оригинал)



    Амбьентное и фокальное зрение


    В современной нейропсихологии существует представлении об амбьентной (от фр. ambiance = окружение) и фокальной зрительных системах. В то время как первая, эволюционно более древняя, ответственна за динамическую пространственную локализацию, вторая занимается идентификацией объектов.


    Таблица. 1. Сравнительные признаки фокальной и амбьентной систем


    Зрительная система Фокальная Амбьентная
    Функция Что Где/Как
    Включенность в движение Меньше Больше
    Осознание/Память Больше Меньше или отсутствует
    Временные свойства Медленная Быстрая
    Чувствительность к освещению Высокая Низкая
    Пространственное разрешение Высокое Низкое

    Объекты, представляющие собой источник нужных сведений, распределены далеко неравномерно. Обычно они локализованы в небольших участках поля зрения. При этом с помощью амбьентного зрения обнаруживается потенциально интересный объект или элемент объекта, а при помощи фокального зрения направленного на объект, эти сведения воспринимаются и анализируются более детально. Сталкиваясь с новой ситуацией или с новым объектом, мы, как правило, сначала смотрим «широким полем» и лишь затем концентрируем наше внимание на деталях.


    Обследование окружения и выбор объектов для детальной обработки осуществляется с помощью движений головы и тела, на которые накладывается тонкий узор движений глаз. Наиболее известной их разновидностью являются саккады — чрезвычайно быстрые (~500°/сек) скачки баллистического типа, меняющие положение глаз в орбите и позволяющие выделять фрагменты сцены для последующей фиксации.


    Рисунок 2. Репродукция картины И. Е. Репина и запись движений глаз испытуемого.¹¹





    Начало исследованиям соотношения амбьентной (глобальной) и фокальной (локальной) зрительной обработки было положено в экспериментах Дэвида Навона в 1977. Он предъявлял испытуемым большие буквы, состоящие из маленьких букв. Некоторые из этих составных стимулов были «однородными» — глобальная форма и локальные элементы представляли собой одну и ту же букву. Другие были «неоднородными» — глобальная и локальные буквы были разными (скажем, «Е» и «S»). Испытуемые должны были как можно быстрее идентифицировать глобальную или локальную букву.


    Рисунок 3. Однородные и неоднородные супербуквы из экспериментов Дэвида Навона.




    Оказалось, что при настройке на глобальную форму она идентифицируется быстро и без всякой интерференции со стороны совпадающих или несовпадающих букв локального уровня. При настройке на идентификацию деталей картина была иной. Во-первых, ответы были более медленными. Во-вторых, в случае неоднородных стимулов ответы дополнительно замедлялись и становились менее точными. Очевидно, настраиваясь на детальную обработку, мы не всегда можем игнорировать глобальную информацию.


    В результате исследований отношения глобальной и локальной обработки, тестируемые с помощью супербукв Навона, была обнаружена возможно дифференциальная роль задних отделов левого и правого полушарий. При этом левое полушарие оказалось скорее регулятором настройки на детали, а правое — на глобальные очертания. Чрезвычайно интересным оказалось влияние эмоций: отрицательные эмоции, в отличие от положительных, усиливали установку на восприятие деталей.


    Законы перцептивной организации


    Интересной особенностью нашего зрения является способность воспринимать группу объектов как единое целое. Так на изображении ниже мы видим собаку, а не просто хаотический набор пятен:


    Рисунок 5. Далматинец




    А здесь мы ясно различаем квадрат и круг¹⁰:


    Рисунок 6. Фигуры из точек.





    Есть множество объяснений возникновения подобных иллюзий. С точки зрения когнитивной биономии потребность видеть формы, грани и движения (а также лица) была продиктована необходимостью в выживании. Таким образом, даже при отсутствии реальных линий или форм наша сенсорно-когнитивная система использовала частичную информацию, чтобы создать эти формы в попытке сделать понятным внешне хаотический мир.


    Посмотрите на рисунок ниже, и вы увидите как с течением времени ориентация треугольников меняется с одного направления на другое, третье.


    Рисунок 7. Треугольники, меняющие ориентацию.¹⁶




    Таким образом можно сказать, что в отсутствии явно выраженной доминантной структуры наш мозг постоянно тратит ресурсы на поиски такой структуры.


    Первыми это явление перцептивной организации стали изучать гештальтпсихологи. Они сформулировали основной закон зрительного восприятия, согласно которому любая стимулирующая модель воспринимается таким образом, что результирующая структура будет, насколько это позволяют данные условия, наиболее простой. Поэтому мы воспринимаем квадрат именно так, как он изображен слева, а не каким-то другим образом:¹⁰


    Рисунок 8. Варианты организации точек в фигуру.




    Гештальтпсихологи также сформулировали 6 принципов перцептивной организации. В соответствии с этими принципами объекты, которые


    • расположены близко друг к другу («закон близости»),
    • имеют похожие яркостные и цветовые характеристики («сходства»),
    • ограничивают небольшую, замкнутую («замкнутости»)
    • и симметричную область («симметрии»),
    • естественно продолжают друг друга («хорошего продолжения»),
    • движутся примерно с равной скоростью в одном направлении («общей судьбы»),

    скорее будут восприняты как единое целое, или фигура, а не как разрозненные элементы среды, или фон.


    Рисунок 9. Примеры сходства по близости, цвету и размеру.¹⁰





    В случае конкуренции нескольких факторов перцептивной организации преимущество, как правило, отдается фактору близости, а затем фактору сходства по окраске, ориентации или размеру.


    Учет этих принципов оказывается важным в случае перцептивного поиска или восприятия, поскольку, если информация организована в соответствии с этими принципами, решение поставленной задачи требует меньших усилий за счет того, что перцептивное поле подвергается группировке, и на образовавшиеся группы элементов последовательно выделяется всё меньшая доля общих ресурсов. Распределение ресурсов внутри каждой группы оказывается примерно равномерным.


    Задачи зрительного поиска обычно затрудняются при добавлении иррелевантных объектов (дистракторов). Однако в случае, когда дистракторы образуют визуально компактные группы, позволяющие игнорировать их как целое, их добавление наоборот может значительно облегчить поиск.


    Асимметрия зрительного поля


    Подобно асимметрии между левой и правой рукой существует некоторая асимметрия в том, как мы воспринимаем левое и правое визуальные поля. Трудно утверждать, связано ли это с асимметрией в левом и правом полушариях мозга, ответственных за обработку сенсорной информации от правого и левого поля зрения соответственно, либо от выработанной привычки.


    Так, в сценическом искусстве известно, что существует разница между левой и правой половиной сцены: как только поднимается занавес в начале акта, зрители начинают смотреть в левую сторону сцены. Левая сторона сцены считается более сильной. В группе из двух или трех актеров тот, кто стоит с левой стороны, будет в данной сцене доминировать. Аналогично, зритель воспринимает рисунок, как если бы он свое внимание сосредоточил на левой стороне. Субъективно он отождествляет себя с левой стороной, и все, что появляется в этой части картины, имеет большое значение. Таким образом, кроме естественной точки равновесия в центре визуальной сцены, формируется дополнительный центр в ее левой части¹⁰.



    Как мы читаем обычные тексты¹


    Когда мы читаем, наши глаза непрестанно совершают быстрые механические (т.е. не контролируемые сознанием) движения, саккады (saccades). В среднем их длина составляет 7-9 символов. В это время мы не получаем новой информации. Основная функция саккад заключается в перемещении новой области текста в область фовеального зрения (2° центральной области видимости) для детального анализа, потому что чтение в области парафовеального или периферического зрения сильно затруднено или невозможно.


    Рисунок 10. Типичная картина движений глаз при чтении





    Между саккадами наши глаза остаются относительно неподвижными на время фиксаций (fixations) (около 200 – 300 мс). В течение этого периода мы распознаем видимую часть текста и планируем, куда совершить следующий cкачок.


    Порядка 10–15% времени читающие переводят свой взгляд назад в тексте (regressions), чтобы повторно прочитать то, что уже было прочитано. С возрастанием трудности текста увеличивается продолжительность фиксаций и частота регрессий, а длина саккад уменьшается.


    Длина саккады определяется в размерах букв, а не угловых величинах, и не изменяется с изменением расстояния от глаза до монитора с текстом.¹⁵


    Во время фиксации мы получаем информацию из области видимости, называемой областью восприятия (perceptual span). Размер этой области относительно невелик: в случае алфавитных орфографий (напр. для европейских языков) она начинается от начала фиксированного слова, но не более, чем на 3-4 буквы слева от точки фиксации, и распространяется приблизительно на 14-15 размеров букв вправо от этой точки (суммарно 17-19 букв).


    Область идентификации (identification span), то есть область видимости, необходимая для идентификации фиксированного слова, меньше, чем область восприятия и, как правило, не превышает 7-8 размеров букв справа от фиксации (суммарно порядка 10-12 букв).


    Доступность первых 3 буквы слова во время предыдущей фиксации приводит к снижению времени фиксации на этом слове. Некоторые исследователи также показали, что информация о буквах справа от фиксации может быть использована для определения должно ли следующее слово быть пропущено.


    Большинство исследователей полагают, что информация о границах слова (обеспечиваемая промежутками между словами) является основным фактором в определении места следующей фиксации. Длина саккады зависит как от длины фиксированного слова, так и от длины слова, следующего за ним.


    В большинстве случаев чтение замедляется (в среднем на 30%) при отсутствии информации о промежутках (между словами). Это вызвано нарушением процессов идентификации слова и перемещения взгляда. Наоборот, добавление информации о промежутках облегчает чтение. Так, существуют данные, показывающие, что при разделении слов в тексте на тайском языке (для людей, которые никогда прежде не читали такие тексты с разделением слов пробелами), чтение осуществляется более эффективно. Аналогично облегчается и чтение длинных составных слов на немецком языке при разбивке их на простые слова с помощью пробелов, несмотря на то, что такое разбиение грамматически некорректно и не встречается в обычном чтении.


    Информация о длине слова также играет явную роль в определении того, где должна располагаться точка фиксации. Хотя присутствует некоторая вариативность в том, где внутри слова останавливается взгляд, как правило первая фиксация на слове осуществляется в предпочитаемой точке взгляда (preferred viewing location), где-то на расстоянии 1/4 длины слова от его начала. Когда промежуток между текущим и следующим словами попадает в парафовеальную область, первая фиксация на следующем слове происходит ближе к предпочитаемой точке, чем когда этот промежуток оказывается за ее пределами.


    Несмотря на то, что в среднем позиция первой фиксации на слове лежит между началом слова и его серединой, эта позиция может меняться в зависимости от расстояния до предыдущей точки фиксации. Например, если расстояние до целевого слова большое (8-10 размеров букв), положение следующей фиксации сдвигается влево. Соответственно, если расстояние мало (2-3 размера букв), положение фиксации сдвигается вправо.


    Позиция первой (и возможно единственной) фиксации на слове лежит между началом и серединой слова для слов длиной 4-10 буквы. Однако для более длинных слов наблюдается тенденция делать первую фиксацию ближе к началу слова и затем вторую ближе к концу слова.


    Информационная плотность (или морфологическая структура) слова влияет на продолжительность фиксаций на каждой части слова. Например, было замечено, что если слово было возможно распознать по первым 6 буквам (слова были в среднем длиной около 12 букв), то в общем случае, после первой фиксации в первой половине слова, взгляд переходил к следующему слову; в случаях, когда фиксация во второй части все же осуществлялась, то она была очень короткой. Однако в случае, когда слово могло быть распознано только по его окончанию, первая фиксация была короткой, а вторая, на конце слова, более длинной.


    Таблица 2. Приблизительные средние значения продолжительности фиксаций и длин саккад при чтении и поиске


    Задача Средняя продолжительность фиксации (мс) Средний размер саккады (градусы)
    Чтение 225 2 (~ 8 букв)
    Чтение вслух 275 1.5 (~ 6 букв)
    Визуальный поиск 275 3
    Восприятие сцен 330 4
    Чтение нот 375 1
    Печать 400 1 (~ 4 буквы)

    При исследовании визуального поиска установлено, что когда цель находилась в области с небольшим эксцентриситетом (отклонением от центра сцены), она обнаруживалась точно, с использованием одной саккады; когда цель располагалась ближе к периферии, наблюдались саккады в ложных направлениях (до 40% по времени). При сложных задачах поиска глаза изначально направлялись к центру сцены и затем к центрам рекурсивно меньших групп объектов до тех пор, пока цель не была найдена.



    Понимание программ


    Программы отличаются от обычных текстов. Программы составлены из ограниченного набора слов и организованны иначе, чем обычные тексты: в них широко используются формально определенные структуры, обозначаемые в тексте с помощью специальных синтаксических конструкций. Кроме того, существует и семантическое отличие. Восприятие обычного текста, в общем случае, состоит из двух параллельных фаз: восприятие самого текста и осмысление того, о чем он повествует. Когда речь идет о тексте программе, осмысление означает осознание синтаксической и семантической структур программы, но также включает и осознание операционной семантики программы, то есть того, как изменяется состояние программы в процессе её выполнения.²



    Когнитивные модели понимания программ³



    Концепции и терминология


    Ментальная модель описывает мысленное представление разработчика о программе, которую необходимо понять, в то время как когнитивная модель описывает познавательные процессы и временные информационные структуры, которые используются для формирования ментальной модели в сознании программиста.


    План программирования (programming plan) – это фрагмент кода, представляющий типичный сценарий в программировании. Например, программа сортировки будет содержать цикл для сравнения двух чисел в каждой итерации. Планы программирования также часто называют клише и схемы. Делокализованный план (delocalized plan) возникает, когда план программирования реализуется в различных частях программы. Наличие делокализованных планов усложняет понимание программ.


    Маяк (beacon) это характерный элемент кода, который служит признаком присутствия в нем некоторой структуры. Например, имя процедуры может указывать на реализацию определенной функции.


    Правила написания программ (rules of programming discourse) охватывают принятые соглашения, такие как стандарты кодирования и реализации алгоритмов. Эти правила формируют определенные ожидания в сознании программиста.



    Модель понимания от общего к частному


    В этой модели предполагается, что процесс понимания программы происходит от общего к частному, когда воссоздание знания о прикладной области программы отображается затем на код программы. Процесс начинается с формулировки гипотезы об общем характере программы. Первичная гипотеза затем уточняется иерархическим способом путем формирования вспомогательных гипотез. Вспомогательные гипотезы уточняются и оцениваются в первую очередь по глубине. Верификация (или отклонение) гипотез сильно зависит от отсутствия или наличия маяков.
    Процесс понимания программы от общего к частному используется, когда код программы или его вид знаком. При этом опытные программисты используют маяки, планы программирования и правила написания программ для декомпозиции целей и планов в планы более низкого уровня.



    Понимание от частного к общему


    Теория понимания программ от частного к общему предполагает, что программисты сначала читают код и затем мысленно группируют утверждения в коде в абстракции более высокого уровня. Эти абстракции в дальнейшем также группируются, и этот процесс повторяется пока не достигается высокоуровневое понимание программы.



    Оппортунистическая и систематические стратегии


    При использовании этих стратегий программисты либо систематически читают код в деталях, отслеживая потоки управления и данных в программе, для получения целостного понимания программы, либо читают его по необходимости, фокусируясь только на коде, относящемся к текущей задаче. В первом случае, программисты получают как статическое знание о программе (информацию о ее структуре), так и знание о причинно-следственных связях в ней (знание о взаимодействии между компонентами при их выполнении). Это позволяет им сформировать ментальную модель программы. При оппортунистическом подходе программисты в основном получают статическое знание, приводящее в результате к формированию более слабой ментальной модели работа программы. Это приводит к большему числу ошибок, так как программисты не могут распознать причинно-следственные связи между компонентами внутри программы.



    Специфика чтения текста программы


    Процесс чтения программы отличается от чтения прозы. Кроме непосредственно чтения текста, программисту приходится также сканировать текст с целью восприятия основных элементов верхнего уровня её иерархической структуры, осуществлять поиск отдельных идентификаторов, переходить от одного места в программе к другому, возможно расположенному в другом файле.


    Сравнительное исследование линейности чтения программ новичками и экспертами² показало, что и те и другие читают код менее линейно, чем обычные тексты. Более того, эксперты читают код менее линейно, чем новички. Авторы предполагают, что навыки нелинейного чтения увеличиваются с опытом.


    Ниже, в качестве примера, дающего представление о различии в чтении обычных текстов и текстов программ, представлены результаты одного эксперимента¹² по анализу движений глаз программистов во время чтения кода.


    Рисунок 11. Траектории движения глаз двух программистов-экспертов при чтении одного и того же кода.




    Перед этими программистами были поставлены разные задачи: так, от первого (рис. слева) ожидали получить ответ, чему равно rect2.area(), второго предупредили, что ему будет задан вопрос относительно алгоритма с возможностью выбрать ответ из списка возможных.


    Как мы можем видеть эти траектории достаточно сильно различаются, что связано, по-видимому, как с разными задачами, поставленными перед ними, так и их индивидуальными особенностями.


    Авторы эксперимента описывают манеру чтения кода первым испытуемым как совершенно непредсказуемую, когда он перескакивал с одного места на другое, практически нигде не задерживаясь на сколько либо продолжительное время.


    Второй же испытуемый, по словам экспериментаторов, читал код медленно и методически.


    Действительно на иллюстрации слева мы наблюдаем достаточно хаотическую траекторию с большим количеством длинных вертикальных саккад, а на правой – преимущественно горизонтальные саккады, большинство из которых можно связать с чтением текста в общем смысле.


    Можно говорить о том, что в первом случае мы видим доминирование быстрого амбьентного зрения, характеризующегося длинными саккадами и короткими фиксациями, а во втором — медленного фокального.


    Авторы описывают следующие базовые типы движений глаз, составляющие более сложные стратегии чтения кода:


    Таблица 3. Базовые типы движений глаз при чтении кода.


    Тип Описание
    Flicking Взгляд перемещается вперед и назад между двумя соотносящимися элементами, такими как списки формальных и актуальных параметров метода.
    JumpControl Взгляд переходит на следующую строку, следуя за потоком выполнения.
    JustPassingThrough Фиксации на свободном месте в процессе перехода куда-то еще.
    LinearHorizontal Целая линия последовательно и равномерно читается целиком слева направо или справо налево.
    LinearVertical Текст читается строка за строкой, как минимум, для трех строк, независимо от потока выполнения программы, не различая сигнатуру и тело функции.
    RetraceDeclaration Частые, повторяющиеся скачки между местами использования и определения переменных. Вид Flicking.
    RetraceReference Частые, повторяющиеся скачки между местами использования переменных. Вид Flicking.
    Scan Первичное поверхностное чтение всех строк кода сверху-вниз. Подготовительное чтение всей программы, которое занимает 30% времени на ее обзор (review).
    Signatures Все сигнатуры функций просматриваются, перед тем как начать изучение тела метода/конструктора.
    Thrashing Взгляд перемещается быстро и неконтролируемо в последовательности, которая кажется не имеет какого-то определенного смысла.
    Word(Pattern)Matching Простое сопоставление визуальных шаблонов.


    Роль идентификаторов


    Идентификаторы в коде программы часто выполняют роль маяков для планов программирования, поддерживающих ментальные модели более высокого уровня. Идентификаторы составляют примерно 70% исходного кода.


    В общем случае, использование целых слов в идентификаторах приводит к лучшему восприятию программы, чем при использовании сокращений.


    Идентификаторы, которые нарушают некоторые правила, приводят к более низкому качеству кода (больше ошибок).


    Использование более длинных имен снижает правильность и требует больше времени для запоминания.


    При сравнении эффективности использования идентификаторов, использующих camelCase нотацию и нотацию с подчеркиванием, в задаче поиска было показано, что хотя вид использованной нотации не влияет на аккуратность результата, использование идентификаторов с подчеркиванием требует меньше времени и усилий для достижения того же результата.


    Рисунок 12. Части траекторий движения глаз при корректном опознании underscore и camelCase идентификаторов, состоящих из трех слов






    Основные принципы форматирования



    Формирование визуальной структуры


    Априори мы знаем, что программа имеет определенную логическую и синтаксическую структуры, и ожидаем, что структура ее визуального представление будет соответствующим образом отражать их.


    Как говорилось выше, наш мозг находится в постоянном поиске некоторого оптимального варианта интерпретации визуальной сцены, позволяющего объяснить ее наиболее простым образом. Поэтому можно утверждать, что, чем более явно сформирована визуальная структура, и чем более точно она отражает структуру программы, тем меньше ментальных усилий мы затратим на восприятие этой программы.


    При визуальном восприятии программы, мы получаем первое впечатление и оцениваем визуальную структуру текста главным образом за счет амбьентного зрения, обладающим малой остротой и цветовосприятием, ухудшающимися от центра к краю. Быстрое амбьентное зрение помогает нам выделить точки интереса в коде для его дальнейшего анализа и чтения с помощью медленного фокального зрения. Это значит, что, говоря об общей визуальной структуре текста программы, мы должны перейти из области фокального зрения, то есть из мира букв и символов, в область амбьентного зрения, то есть в область пятен и соотношений между ними.


    Последовательность основных структурных элементов в программе располагается в вертикальном направлении, поэтому и при оценке общей визуальной структуры программы при охвате её «широким взглядом» преобладает вертикальное движение глаз, сопровождающееся небольшими отклонениями по горизонтали. Горизонтальное движение взгляда, связанное с переходом к фокальному зрению и непосредственно чтению, позволяет нам получить детали этих элементов.


    Соответственно, для корректной оценки общей структуры программы важно сформировать корректную визуальную структуру в вертикальном направлении.


    Например, в определении функции нам необходимо визуально разделить объявление функции, включающее её имя, возвращаемый тип, список параметров, и тело функции. Внутри тела функции необходимо разделить код инициализации начальных переменных, тело основного алгоритма, формирование и возвращение результата. В свою очередь, внутри кода инициализации нам надо разделить область типов, имен переменных и присваиваемых им значений.


    Рассмотрим следующий пример:




    Для оценки визуальной структуры проведем тест, аналогичный «тесту с прищуриванием» (squint test), используемому разработчиками пользовательского интерфейса. Алан Купер в своей книге описывает этот тест так¹⁸:


    Закройте один глаз и посмотрите на экран прищуренным вторым глазом. Обратите внимание на то, какие элементы слишком выпирают, какие стали нечеткими, а какие объединились в группы. Эта процедура часто вскрывает не замеченные ранее проблемы в композиции интерфейса.

    Фактически, с помощью прищуривания мы пытаемся оценить, как мы воспринимаем изображение посредством амбьентного зрения. Вместо прищуривания можно попробовать посмотреть на код расфокусированным взглядом и несколько в сторону от интересующего нас фрагмента.





    Визуальная структура этого кода содержит лишь три большие области, что, очевидно, не отражает корректно структуру программы.


    Разбивка по вертикали позволяет исправить этот недостаток:




    Результат теста:





    Связанные области текста могут состоять из строк одинаковой структуры. В этом случае имеет смысл сгруппировать одинаковые элементы строк, подчеркнув тем самым их горизонтальную структуру. Такая группировка позволит выделить общее и сделает различия более заметным.


    Таким образом, мы структурируем текст как по вертикали, так и по горизонтали. В первом мы случае выполняем эти разделения посредством добавления пустых строк. Во втором – мы используем отступы и выравнивание.


    Отступы служат для формирования визуального представления иерархии в структуре программы между элементами, находящимися на разных строках.


    Давайте посмотрим на следующий код:




    Список аргументов представлен в виде колонки и имеет отступ относительно первой строки. Достаточно ли этого отступа для того, чтобы правильно отобразить логическую структуру программы? Очевидно, что нет.


    Правило сходства по близости связывает список аргументов с именем переменной сильнее, чем с именем функции даже несмотря на то, что благодаря подсветке синтаксиса формируется сходство по цвету. Но что случится, если алгоритм подсветки изменится? Вот как выглядел этот же код на gitlab:




    В данном случае подсветка синтаксиса еще больше ухудшила ситуацию, поскольку правило сходства по цвету теперь также усиливает связь аргументов с именем переменной. Имя функции здесь как бы и ни при чем.


    При сканировании текста, такое расположение провоцирует движение глаз от результирующей переменной success сразу к столбцу списка аргументов и только потом регрессию к имени функции.


    Подсветка синтаксиса может значительно облегчить восприятие программы. Однако, как мы видим из этого примера, в случае некорректной визуальной структуры, эффект от нее может быть совершенно противоположный. Учитывая также, что поскольку программист не контролирует подсветку синтаксиса, нельзя принимать ее во внимание при оценке того, насколько удобочитаемым и правильно отражающим структуру программы является её конкретное визуальное представление.


    Чтобы правильно отобразить логическую структуру этого кода в его визуальное представление, необходимо, чтобы список аргументов имел отступ не относительно начала строки, а относительно начала имени функции:




    В этом варианте явно видно, что список аргументов функции синтаксически является частью выражения вызова функции и логически представляет собой набор параметров, который преобразуется посредством вызова функции.


    Проведем тест с прищуриванием. Отключим также подсветку синтаксиса для того, чтобы исключить группировку по фактору сходства по окраски, которую мы не контролируем:





    Видно, что имя функции и список аргументов формируют сильно связанную область. Однако при использовании двух пробелов для отступа подчиненность списка аргументов относительно имени функции выглядит недостаточно выразительно. Использование четырех пробелов помогает решить эту проблему:





    Остается еще одна проблема: список аргументов выглядит как одно неструктурированное пятно, несмотря на то, что в нем присутствуют два типа элементов, метки аргументов и их значения, то есть, этот список обладает определенной структурой. Для того, чтобы визуально выделить эту структуру, используем выравнивание. Но перед этим отвлечемся на некоторое время от этого примера и рассмотрим различные способы форматирования списка аргументов в виде метка: значение.


    Однострочное форматирование




    В этом варианте визуальная структура не отражает структуру выражения, поэтому поиск затруднен. Этот вариант предназначен лишь для того, чтобы его тщательно читали.


    Из-за большой длины строки остаток ее, не помещающийся на первой строке, был помещен на вторую. В данном случае среда разработки сделала отступ, показывающий подчиненность этой строки относительно предыдущей. Однако, в другом месте (например, в панели сравнений на gitlab) вторая строка скорее всего будет начинаться с начала и визуальная структура участка кода, включающего эту строку, будет поломана.


    Выравнивание пар метка: значение




    Этот вариант выглядит лучше в том смысле, что в нем спецификация имени метода и список аргументов явно отделены друг от друга. Уже несколько проще поиск в списке аргументов, поскольку каждая пара располагается на отдельной строке и метки выровнены по левому краю.


    Тем не менее различение меток и значений затруднено из-за того, что отсутствует явная граница между областями меток и значений для всего списка. Кроме того, поскольку текст воспринимается как зафиксированный слева и свободный справа, в визуальном представлении этот код выглядит как большой груз (область списка аргументов), подвешенный на тонком и длинном рычаге. Основная масса кода оказывается сильно смещена в правую, менее важную часть пространства.


    В данном случае можно попробовать исправить последний недостаток, сместив список аргументов влево:




    Для лучшего представления внутренней структуры списка аргументов необходимо сформировать явную разделительную линию между областями меток и значений. Здесь у нас есть два варианта.


    Вариант 1: метки и аргументы выровнены влево по отдельности




    Из всех предложенных до сих пор вариантов, этот наиболее точно передает логическую структуру кода. В нем явно различимы области меток и значений, идентификация и поиск отдельных элементов внутри их значительно облегчается. Однако из-за большой разницы в длинах меток аргументов связи внутри пар с короткими метками оказываются ослабленными.


    Вариант 2: внутреннее выравнивание




    Обладая всеми преимуществами предыдущего варианта, этот вариант для данного примера выглядит более естественным. В нем метки аргументов выглядят как естественное продолжение имени метода, как это и должно быть в данном случае. Код выглядит достаточно компактным и без необходимости переноса списка аргументов на следующую строку.


    Для примера с reticulateSplines код в этом варианте оформления будет выглядеть следующим образом:




    Выравнивание является мощным средством оптимизации визуального представления кода, за счет формирования компактных групп в горизонтальном направлении.


    Разберем следующий код:




    В принципе, каждое отдельное объявление протокола выглядит достаточно удобочитаемым. Однако сказать такое обо всем тексте нельзя: общая структура невыразительна, в ней отсутсвуют явно выраженные массы, которые могли бы притянуть взгляд. В результате «в глазах рябит», и есть ощущение монотонности. Чтобы воспринять этот код его можно только читать последовательно и полностью: «protocol DataBaseDependent ServiceDependent var dataBase DataBase get set, protocol LocalConfigDependent…»


    Переформатируем его так, чтобы каждое объявление занимало одну строку и выровняем:




    При этом в горизонтальном направлении сформировались явно выраженные группы (группировка по «близости»), явно проявилась структура каждого объявления и различия между ними. Отличается и способ чтения этого кода. После быстрого первичного ознакомления со структурой и вычленения общих частей, при дальнейшем разборе эти общие части просто игнорируются, и внимание концентрируется только на различающихся частях. Таким образом, за счет более активного использования амбьентного зрения снижается нагрузка на фокальное и уменьшается количество умственных усилий, требуемых для понимания программы.


    Как упоминалось выше, существует некоторая асимметрия в том, как человек воспринимает левую и правую части визуальной сцены. Эта асимметрия еще более естественным образом присутствует в восприятии текста программы: текст жестко привязан к левому краю, где отступы задают уровень иерархии в логической структуре программы. Правый же край свободен и не имеет жесткого ограничения. Чтение слева направо и сверху вниз определяет то, что что-то новое мы ожидаем увидеть сверху и слева или по центру.


    В этом смысле нельзя назвать удачными с точки зрения читаемости конструкции вида:




    В таких конструкциях новое пространство имен начинается в конце строки, то есть в области наименьшей важности, там где это начало не ожидаемо. Более естественно расположить начало этого блока кода там, где он и должен быть — вверху слева:





    Длина строки


    Отсутствие жесткого ограничения справа, не означает, что ограничения нет вообще. Многовековой опыт книгопечатания,¹³ и десятилетия опыта, наработанного веб-дизайнерами, сходятся к тому, что оптимальная длина строки, обеспечивающей комфортное чтение составляет приблизительно 45-75 символов.


    Несмотря на структурные отличия текстов программ, трудно вообразить, что эти отличия настолько сильны, что могут сделать длинные строки, трудные для чтения обычных текстов, легкими в случае, когда мы читаем программу. Наоборот, можно ожидать, что программы, подобно научным изданиям, требуют более коротких строк, чем проза.¹⁷


    Видимо, повторяя Стива Макконнелла¹⁴, некоторые разработчики говорят, что на их больших мониторах длинные строки прекрасно помещаются, и их присутствие в коде нормально. Этот аргумент «больших мониторов» не выдерживает никакой критики:


    • Длинную строку трудно охватить «широким взглядом».
    • Оценка структуры выражения затруднена из-за того, что эта структура размазана по строке и не формирует явно выраженных компактных визуальных областей.
    • Из-за неявно выраженной структуры и увеличения эксцентриситета затрудняется поиск.
    • Из-за увеличения расстояния затрудняются переходы от конца строки к началу следующей, и соответственно замедляется чтение.
    • Если отодвинуться от монитора с целью уменьшения углового размера строки, чтобы нивелировать некоторые указанные выше проблемы, это затруднит чтение за счет уменьшения размера букв и расстояний между ними. При этом это размер области идентификации и распознавания и размер саккад, измеряемых в буквах, не изменится. То есть, чтение замедлится.
    • Большие мониторы не всегда доступны или текст программы может отображаться в области с гораздо меньшей шириной. При этом строка или не помещается в области видимости и требует прокрутки, либо строка разбивается на несколько строк и это, как правило, разрушает структуру программы во всей области видимости.

    По ряду объективных причин не всегда можно избежать длинных строк (например, из-за использования длинных идентификаторов, которые мы не в силах изменить). Также в случаях, когда нас не интересует структура выражения (например, при выводе отладочного сообщения в лог программы), использование длинной строки может оказаться даже предпочтительнее структурирования длинного выражения разбивкой его на несколько строк, так как делает этот код менее значимым с точки зрения амбьентного зрения.


    В общем же случае, длинные строки, как и длинные идентификаторы, это признак плохой читаемости кода.



    Имена


    Имена играют важнейшую роль для обеспечения удобочитаемости программного кода. Они занимают его большую часть и часто играют роль маяков, позволяющих идентифицировать характерные структурные части программы. Основные требования к именам – это их краткость и выразительность. Чем имя длиннее, тем оно труднее для чтения, запоминания и поиска. Длинные имена, как правило приводят к длинным строкам, что тоже затрудняет чтение и поиск. Требование выразительности означает, что в области контекста использования, имя должно позволять однозначно определять роль обозначаемого им элемента программы.


    Требования краткости и выразительности могут очевидным образом конфликтовать друг с другом, поскольку выразительность может требовать использования более длинных, составных имен. Поэтому имеет смысл сделать оценку допустимой рекомендованной длины имени.


    В идеале, мы хотим распознавать имя с первого взгляда (при первой фиксации). Это значит, что первую оценку оптимальной длины можно заложить как размер области идентификации, то есть 10-12 символов. Особенностью текстов программ, как уже писалось выше является то, что набор допустимых слов в них ограничен, поэтому велика вероятность, что даже в случае более длинного имени мы сможем впоследствии распознавать его по первой части, так что в принципе, даже при длине больше 12 символов нам потребуется лишь одна фиксация. Мы, однако хотим, чтобы при этом это имя помещалось в размер области распознавания (17-19 символов) и оставался некоторый запас, таким образом, чтобы наш мозг имел возможность оптимально спланировать следующую саккаду. Если мы возьмем 4 символа от конца области распознавания, то получим оценку в 13-15 символов.


    Допуская в редких случаях две фиксации с «угадыванием» мы получим оценку в 20-24 символа (13-15 из предыдущей оценки + 7-9 на саккаду внутри слова) .


    Взяв середины отрезков полученных оценок получим следующую таблицу:


    Таблица 4. Оценки максимальной длины имени.


    Число фиксаций Оценка максимальной длины имени
    1 11
    1 (с угадыванием окончания) 14
    2 22

    Эти рекомендации достаточно хорошо согласуются с теми границами, которые приводит в своей книге¹⁴ Стив Макконнелл: 10-16 и 8-20. Теперь мы можем как-то объяснить их.


    На практике иногда приходится использовать имена, длины которых выходят за предлагаемые пределы. Например, когда имя включают в себя некоторое стандартное именование группы, к которой относится данный элемент, как в PreferencesViewController. Располагая значимую, уникальную часть имени в начале, мы можем ожидать, что распознаем уникальную часть имени уже при первой фиксации, и в то же время нам не потребуется больших усилий для распознания «стандартного дополнения».


    За редким исключением не имеет смысла использовать в именах какие-либо префиксы, описывающие некоторые общие характеристики (например тип) или для различения классов, являющихся частью вашего приложения. Префиксы маскируют смысловую часть имени, в их присутствии позиция первой фиксации на слове при чтении сдвигается влево от оптимальной, они требуют определенных усилий на дополнительный анализ слова. В некоторых случаях они могут изменять значение имени (kBytesPerSec это «килобайты в секунду» или константа BytesPerSec?).


    Декорирование имен классов и функций имеет смысл лишь в случае разработки библиотеки на языке, в котором отсутствует понятие пространства имен, позволяющих ограничить их видимость. Все сущности, определяемые внутри вашего приложения, находятся на верхнем уровне пространства имен и, как правило, не нуждаются в каких-либо префиксах для предотвращения конфликтов имен.



    Пробелы


    Как указывалось выше, использование любого другого разделителя между словами вместо пробела требует больших усилий при чтении, поскольку приводит к затруднениям при определении границ слова, что в свою очередь затрудняет его распознавание и планирование следующей саккады.


    Поэтому рекомендуется разделять идентификаторы в программе с помощью пробелов, даже в случае, если формально такого разделения не требуется. Например, имеет смысл разделять пробелом имя функции и список ее параметров/аргументов:




    В первой строке пробел отсутствует, и имя функции сливается с первым аргументом в списке аргументов. Кроме затруднения в чтении, мы можем также заметить, что визуальная структура не совсем корректно отражает логическую структуру программы: выражение вызова функции включает в себя имя функции и список аргументов, список аргументов включает в себя первый и второй аргументы. В первой же строке имя функции сильнее связано с первым аргументом, чем аргументы между собой.


    Еще один пример:




    После разделения на две группы, добавления пробелов и выравнивания:




    В данном случае добавление пробелов не только облегчило чтение отдельных строк за счет явного разделения идентификаторов внутри их, но и (вместе с выравниванием) облегчило их сравнение, посредством образования компактных визуальных групп по вертикали. Для того, чтобы понять, что делает этот код, уже не надо читать отдельно каждую строку.


    В случае, когда совокупная длина идентификаторов не превышает размера области распознавания, это требование не является настолько критическим, поскольку все выражение может быть сразу охвачено одним взглядом:





    Расстановка фигурных скобок.


    На сегодняшний день в языках с Си-подобным синтаксисом доминируют два основных способа расстановки фигурных скобок: в первом открывающая скобка находится на отдельной строке с тем же отступом, как и начало связанного с ней предшествующего синтаксического элемента, а во втором открывающая скобка располагается в конце строки, содержащей окончание такого элемента.


    Далее я буду условно называть эти стили Allman и One Truce Brace Style (1TBS) по названиям наиболее популярных стилей, которые используют соответствующие правила расстановки скобок.


    Расположение открывающей скобки в начале отдельной строки в стиле Allman обладает следующими преимуществами:


    • Скобка всегда располагается в левой части визуальной области, т. е. в области наибольшего внимания, и при сканировании кода (в котором преобладают вертикальные движения глаз) всегда так или иначе попадают в область фокального зрения. В стиле 1TBS открывающие скобки часто оказываются в правой области кода и попадают лишь в область периферийного зрения, что значительно затрудняет их обнаружение. Другими словами, то что в первом случае происходит естественно и как бы само по себе, в втором требует дополнительных и специальных усилий.
    • Облегчено сопоставление открывающей и закрывающей скобок и, соответственно, определение границ обрамленного ими блока кода. Поиск парной скобки требует лишь вертикального перемещения взгляда, на пути его следования от одной скобки к другой нет никакого текста, и поиск происходит в известном направлении до первого символа.
      Действительно, всегда, когда мы видим в тексте закрывающую скобку на отдельной строке, мы знаем, что парная ей открывающая находится выше, а это значит, что основным и естественным направлением поиска её будет поиск вверх.
      При использовании 1TBS, поиск в общем случае требует больших усилий за счет того, что он осуществляется в широком секторе, причем взгляд проходит через текст, который надо анализировать, и который часто содержит вложенные пары фигурных скобок, визуально конкурирующие с целевой.
    • Скобка расположена в начале строки, именно там, где мы ожидаем увидеть начало чего бы то ни было, и естественным образом обозначает начало блока кода. В стиле 1TBS из-за своего положения в конце строки открывающая скобка часто перестает играть роль явного визуального маркера начала блока. Более того, в некоторых случаях (напр. повторяющиеся конструкции } else if) закрывающая скобка предыдущего блока оказывается в начале строки, содержащей конструкцию, предваряющую новый блок, и тем самым визуально связывается с началом этого блока. Таким образом, её формальное и визуальное значения перестают соответствовать друг другу.
    • Расположение скобок на отдельных строках, естественным образом добавляет вертикальные пробелы между синтаксической конструкцией перед открывающей скобкой и обрамленным блоком кода, что, в большинстве случаев, лучше отражает структуру всей конструкции.
    • Горизонтальная позиция открывающей скобки в стиле Allman однозначно определяет уровень вложенности, к которому она относится. В 1TBS эта информация отсутствует, так как расположение открывающей скобки в основном определяется лишь длиной строки, расположенной перед ней.

    Недостатки 1TBS приводит к тому, что открывающая фигурная скобка перестает в полной мере участвовать в формировании визуального представления кода, стиль провоцирует программиста не разделять блоки кода внутри скобок и окружающие их элементы пустыми строками, и в результате текст программы часто выглядит как один плохо структурированный массивный кусок:


    picture36


    Код в примере выше демонстрирует основные упомянутые проблемы стиля 1TBS, а именно: отсутствие явно выраженной визуальной структуры, инвертирование роли закрывающих скобок, потеря визуальной значимости открывающих, которые при беглом взгляде на код лишь приблизительно угадываются с помощью периферийного зрения и их точная локация требует дополнительных горизонтальных движений глаз. При этом этот код еще можно назвать простым с той точки зрения, что в нем условия выражения if занимают всего одну строку, и открывающие скобки находятся на той же строке, что и if, а блоки кода внутри скобок имеют достаточно простую линейную структуру и не содержат вложенных блоков.


    Переформатирование этого кода с использованием стиля Allman позволяет получить более приемлемый результат:


    picture37


    Несмотря на то, что в большинстве случаев Allman объективно выигрывает у 1TBS, иногда 1TBS  оказывается предпочтительнее. Как правило, это связано с тем, что в таких случаях дополнительное вертикальное пространство, образуемое за счет расположения скобок на отдельных строках в стиле Allman, приводит к тому, что вся конструкция становится визуально разрозненной, теряет свой внутренний ритм и перестает восприниматься как единое целое. И в то же время, при использовании 1TBS, либо смещение открывающей скобки невелико и не оказывает значимого влияние на восприятие кода, либо её обнаружение непринципиально (например, в случае конструкции if, когда и условие, и блок кода занимают по одной строке).


    Так на предыдущем примере, постановка последней открывающей скобки в конце строки выглядит достаточно естественно, и образовавшееся пустое пространство лишь добавляет небольшой акцент, компенсирующий малый визуальный объем последнего блока, состоящего всего из одной строки. При расположении же скобки на отдельной строке это пространство становится слишком большим, и эта строка выглядит уже оторванной от остальной конструкции:



    Возникает вопрос: в каких случаях допустимо использовать 1TBS? Здесь можно предложить следующие ограничения:


    • Открывающая скобка располагается на той же строке, что и начало синтаксической конструкции, частью которой она является.
    • Скобка не замаскирована большой массой кода непосредственно примыкающего к ней (в основном сверху).
    • Она располагается в левой области текста (области наибольшего внимания).
    • Смещение скобки по горизонтали не должно быть большим, так что скобка оказывается в области распознавания (14-15 размеров букв).
    • Блок, ограниченный скобками, не содержит вложенных блоков таких, что скобки, ограничивающие эти вложенные блоки, расположены близко к открывающей скобке основного блока и визуально конкурируют с ней.

    Таким образом, выбор способа расстановки скобок должен осуществляться в каждом конкретном случае, а формальное следование «жестким» правилам расстановки рано или поздно приводит к неудовлетворительному результату. Наиболее оптимальным вариантом видится комбинация стилей Allman (как основного) и 1TBS (как вспомогательного, используемого в редких случаях) .



    Заключение


    Формирование удобочитаемого, то есть легкого для восприятия текста программы требует учета специфических особенностей зрения человека, таких как амбьентное и фокальное зрение, механизмов чтения текста вообще и особенностей чтения текстов программ в частности.


    Основную стратегию оптимизации удобочитаемости можно сформулировать как стремление к более эффективному использованию амбьентого зрения и снижения нагрузки на фокальное.


    Реализация этой стратегии достигается за счет формирования текста в виде относительно компактного по горизонтали «изображения», обладающего ярко выраженной визуальной структурой, корректно отражающей структуру программы. Это изображение формируется за счет иерархического группирования логически связанных элементов программы в компактные визуальные области посредством горизонтальных отступов, добавления пустых строк и выравнивания. Легкость чтения текста программы также обеспечивается выбором оптимальных по длине и выразительности идентификаторов и явного разделения их в тексте программы с помощью пробелов.


    В своей книге о типографике Роберт Брингхерст пишет¹³:


    Заголовки, подзаголовки, большие цитаты, сноски, иллюстрации, подписи к ним и другие включения, набираемые в разрез текста, создают своеобразные синкопы и вариации в противовес основному ритму строк… Эти вариации могут и должны вдохнуть жизнь а страницу…

    Чтобы книга, то есть длинный текст, была удобна для чтения, страница должна дышать в обоих измерениях.

    Аналогично можно сказать, что структура программы, разворачиваясь в тексте сверху вниз, также обладает определённым ритмом и вариациями. Задача программиста — визуализировать этот ритм, сделать его явным в общем и в деталях.




    ¹) Eye Movements in Reading and Information Processing: 20 Years of Research. Keith Rayner – University of Massachusetts at Amherst
    ²) Eye Movements in Code Reading: Relaxing the Linear Order. Roman Bednarik, Bonita Sharif
    ³) Theories, tools and research methods in program comprehension: Past, Present and Future. Margaret-Anne Storey
    ⁴) An Eye Tracking Study on camelCase and under_score Identifier Styles. Bonita Sharif and Jonathan I. Maletic – Department of Computer Science Kent State University
    ⁵) Achieving Software Quality through Source Code Readability, Phillip Relf
    ⁶) Relating Identifier Naming Flaws and Code Quality: An Empirical Study. Simon Butler
    ⁷) Грегори Р.Л. Глаз и Мозг. М.: Прогресс, 1970
    ⁸) Дэвид Хантер Хьюбел. Глаз, мозг, зрение. Мир, 1970
    ⁹) Величковский Б.М. Когнитивная наука. Основы психологии познания. Academia, Смысл, 2006
    ¹⁰) Рудольф Арнхейм. Искусство и визуальное восприятие. М.: Архитектура-С, 2012
    ¹¹) Ярбус А.Я. Роль глаз в процессе зрения. М.: Наука, 1965
    ¹²) Eye movements in programming education: analysing the expert’s gaze. Simon. University of Newcastle, Australia
    ¹³) Роберт Брингхерст. Основы стиля в типографике. М.: Дмитрий Аронов, 2006
    ¹⁴) Макконнелл С. Совершенный код. М.: Русская редакция, 2010
    ¹⁵) Saccade size in reading depends upon character spaces and not visual angle. Robert E. Morrison, Keith Rayner, 1981
    ¹⁶) Роберт Солсо. Когнитивная психология. СПб: Питер, 2002/2006
    ¹⁷) ОСТ 29.124–94. Издания книжные для взрослых читателей.
    ¹⁸) Алан Купер. Алан Купер об интерфейсе. Основы проектирования взаимодействия. – Пер. с англ. – СПб.: Символ-Плюс, 2009
    EPAM
    Компания для карьерного и профессионального роста

    Комментарии 47

      +1
      Крутой труд, автору спасибо. Даже не программисту будет интересно, особенно если человек занимается разработкой GUI.
        +1
        Действительно глубоко.
        Особенно про фигурные скобки здорово. Из общего соотношения преимуществ/недостатков складывается впечатление почти абсолютной победы Allman (да и прямо сказано: «в большинстве случаев Allman объективно выигрывает у 1TBS»). В реальности, кажется, он не популярнее соперника (кстати, было бы интересно посмотреть статистику).
        Наиболее оптимальным вариантом видится комбинация стилей Allman (как основного) и 1TBS (как вспомогательного, используемого в редких случаях) .

        Внутри одного проекта, наверное, было бы странно. Тем не менее, сформулированы какие-то правила оптимального выбора.
          0
          Внутри одного проекта, наверное, было бы странно.

          А почему «странно»? Могу понять, что в чем-то трудно, но все равно решаемо.

            0
            Обычно принимаются же какие-то соглашения на уровне проекта по форматированию. Удачные или не очень, но единообразные. Далее моя субъективная оценка. Позитивная сторона этого единообразия — соответствие эргономическому принципу «наименьшего удивления». Мозг читающего код работает в одном режиме, без внезапных переключений. И этот плюс перевешивает плюсы «локально адаптируемых» стандартов.
              0
              Решил посмотреть, что на эту тему сказано в классике, поскольку мой собственный ответ показался мне слабо или не очень внятно аргументированным. Открываю цитируемый Вами в начале статьи «Совершенный код» Р. Мартина.
              Раздел «Правила форматирования в группах», всего половина страницы.
              «У каждого программиста есть свои любимые правила форматирования, но если он работает в группе, то должен руководствоваться групповыми правилами. Группа разработчиков согласует единый стиль форматирования...» Далее рассказ из личного опыта работы в команде.
              Ну и в заключение дублируется то, что сказано в начале. «Должен» и «не должен», без обоснований. Классика позиционирует как аксиому.
              Кстати, в этой же книге фигурные скобки расставлены в худшем по версии Вашей публикации стиле. Аналогично в «Паттернах» Банды Четырех и новых «Паттернах» (Фримен и др.). Это просто наугад три взятые с полки книги.
                0

                Ну, у Мартина я цитирую то, с чем согласен. Проблема в том, что часто те правила, которые приводят «классики», не имеют под собой каких-то ясных объяснений. Я попытался эти правила найти. Цитата, которую вы приводите характерна тем, что в ней автор оперирует такими понятиями как «любимый», то есть он априори говорит лишь о субъективной составляющей, и оставляет в стороне объективные критерии удобочитаемости.


                По поводу скобок, если они стоят на своем месте, то есть не вызывают затруднений при чтении и формируют корректную визуальную структуру, то удивления вызывать они не будут. Почему они должны вызывать у вас удивления, если вы готовы к тому, что в ваших правилах есть известные исключения? Расстановка же по жесткому правилу, которое принципиально не способно учесть весь контекст текста, всегда в большей (для 1TBS ) или меньшей (для Allman ) степени будет приводить к неудовлетворительному результату.


                Соглашения должны быть, но формироваться они должны на основе понимания того, почему мы объективно должны делать так, а не иначе, а не по праву первого или в результате простого следования тому, что пишут «классики».

                  0
                  Эргономика — наука экспериментальная. Утверждение вполне себе в духе обсуждаемой публикации. Количественные характеристики зрения, маршруты движения глаз — не философия.
                  Мое оценочное суждение о правильном соблюдении принципа наименьшего удивления и Ваше возражение
                  Почему они должны вызывать у вас удивления, если вы готовы к тому, что в ваших правилах есть известные исключения?

                  оба относятся больше к философии. До того момента, когда эксперимент по сравнению качества деятельности программиста при работе с кодами в двух стилях подтвердит одно из двух.
                  Также эксперимент может показать, что это вообще никакой роли не играло:)
                    0

                    Если удалить философию и рассуждения об экперименте, которое не проводилось, то у нас останутся лишь факты, относящиеся к психо-физическим особенностям зрения. А они ничего не говорят нам о том, что мы должны всегда ставить скобки только одним, заранее выбраным способом. По моему, это единственно правильный способ рассужения. :)

                      0
                      Придумал пример не про скобки, но про однообразие. Если поднапрячься, можно и со скобками что-то похожее выдать.

                      if (условие1) действие 1;
                      действие 2;
                      действие 3;
                      if (условие2) действие 4;
                      if (длинное-предлинное, уходящее за край экрана условие3)
                      действие 6;
                      действие 7;

                      Действие 6 выполняется всегда или только при условие 3==true? А если при беглом просмотре такого кода?
                        0

                        Прошу прощения, вопрос не понял.

                          0
                          Псевдокод такой, Си-подобный. «условие n» — какая-то булева величина, «действие m» — statement, номера m и n для идентификации элементов (надо было слитно писать, типа «действие_6», но уже с пробелами сделал).
                          Представьте, что Вы читаете этот код.
                          Первая строка — условие и действие записаны в одну строку. Так делают. Чаще в javascript, мне кажется, но и в C/С++ и других языках допустимо и встречается. С первым if все ясно: действие 1 выполняется, если (условие 1). Действия 2 и 3 выполняются в любом случае.
                          Второй if — аналогично, выполнится действие 4, если (условие 2).
                          И доходим до следующего if. Можно подумать, что действие 6 выполнится в любом случае, поскольку в предыдущих случаях код «ниже if» не относился к if.
                          То есть, я уже воспринимаю код в режиме, когда одна конструкция if умещается целиком в одной строке. Вот и повод удивиться, когда действие 6 не захочет выполняться (с чего бы это? всего-то условие 3 == false).
                          Кстати, вполне вероятный пример. Встречал рекомендацию именно короткие стейтменты писать в однострочном варианте. Один в один как в этом примере выходит.
                          С адаптивным правилом по фигурным скобкам возможны вполне себе похожие на эту ситуации.
                            0

                            Псеводкод я понял. Да, тут можно так подумать. Но это потому, что код некорректно отформатирован (не отражает структуру программы, действие 6 должно иметь отступ). И плохо написан в том смысле, что действие 6 надо по-хорошему заключать в кавычки.


                            Как это относится к правилам расстановки фигурных скобок, я так и не понял.


                            Предлагаю взять паузу и обдумать всё хорошенько. Тем более что по результатам нашей беседы у меня появились две небольшие формулировки, которые может стоить вставить в статью. :) Надо обдумать.

                              0
                              И плохо написан в том смысле, что действие 6 надо по-хорошему заключать в кавычки.

                              В кавычках в коде — текстовый литерал получится вместо стейтмента. Уверен, Вы не это имели ввиду, но не пойму, что именно.
                              Предлагаю взять паузу и обдумать всё хорошенько.
                              Работа так-то и начинается с перекура)
                              Тем более что по результатам нашей беседы у меня появились две небольшие формулировки, которые может стоить вставить в статью

                              Значит, все было не зря)
                                0
                                В кавычках в коде — текстовый литерал получится вместо стейтмента. Уверен, Вы не это имели ввиду, но не пойму, что именно.

                                Ох и ах. Фигурные скобки, конечно.

                                  0
                                  По фэншую нужны, конечно.
          0
          В данном случае добавление пробелов не только облегчило чтение отдельных строк за счет явного разделения идентификаторов внутри их, но и (вместе с выравниванием) облегчило их сравнение, посредством образования компактных визуальны групп по вертикали. Для того, чтобы понять, что делает этот код, уже не надо читать отдельно каждую строку.

          При отделении названия функции пробелом её название начинает восприниматься как переменная в математическм выражении. И скобок закрывающих не хватает.
            0
            Скобки поправил. Спасибо.
              0
              При отделении названия функции пробелом её название начинает восприниматься как переменная в математическм выражении.

              Я бы прокомментировал это так: этап выделения и чтения идентификаторов находится на более низком уровне, по сравнению с этапом определения их роли, то есть разбора синтаксического выражения. Первый этап явно выигрывает от наличия пробелов, и это определяется нашими психо-физическими особенностями восприятия (т.е. не может быть изменено). То, как интерпретируется выражение, это результат научения и выработанной привычки, и, следовательно, может быть изменено. Мы можем привыкнуть легко читать тексты программ, написанные на разных языках программирования, но отсутствие явного разделения между идентификаторами всегда будет требовать больших ментальных усилий.
                0
                Сравните:
                b = a * a ( a (a, a), a);
                b = a * a ( a (a,a), a);
                b = a * a( a( a, a ), a );
                b = a * a(a(a,a), a);
                  0

                  Не понимаю, что сравнить, но если выбрать из того, что есть, то последний.

                    0
                    Восприятие.
                    Первая строка с пробелами читается так:
                    а умножаем на а, потом делаем что-то в скобках.
                    Пробел в тексте привычно воспринимается как пауза, отделение нового смыслового блока. Поэтому отделение названия функции пробелом от скобки дробит восприятие.
                      0

                      Относительно первой строки. Во-первых, лишний пробел после первой скобки. Он отрывает аргумент от имени функции.


                      b = a * a (a (a, a), a)

                      В принципе, я могу увидеть здесь и a * a с хвостом, но с большей легкостью это интерпретируется правильным образом (возможно, потому что я знаю, что это правильно).


                      Во-вторых, имена короткие. Я бы даже сказал нереально короткие. Размер пробела оказывается большим по сравнению с размерами идентификаторов, и они воспринимаются как далеко отстоящие друг от друга. Да и оператор весит не намного меньше имени. Возможно из-за этого имя легче отрывается от списка аргументов.


                      Более реальный вариант выглядел бы как-то так:


                      result = firstOperand * functionName (firstArgument, secondArgument)

                      Здесь в масштабе имен пробел уже не так весом. При желании, конечно, можно и тут напрячься и проинтерпертировать это как firstOperation * functionName, но мой мозг сопротивляется ("Там же список аргументов!")

                        0
                        а так?
                        result = max * max (min, max)
                          0

                          Все то же. Из-за масштаба просто пробелы выглядят большими. Но список аргументов все равно попадает в поле зрения и учитывается при интерпретации выражения.


                          С вариантом без пробела, тоже, казалось бы, не дожно быть каких-то затруднений при чтении, учитывая малые длины:


                          result = max * max(min, max)

                          Но тем не менее при первом взгляде на это выражени у меня лично все равно возникает некоторый дискомфорт из-за слипшихся max(min, которые, если смотреть куда-нибудь в строну result или первого max, воспринимаются как что-то единое, с каким-то возмущением посередине.

                            0
                            Речь была о том, что в выражениях переменные будут носить имена подходящие под смысл выражения, а вовсе не operand или functionName.
                            result = min_current * duty_cycle (vin_min)

                            Как вариант: префиксы еще добавлять.
                              0

                              Ну, получается, что я вас не понял. И сейчас не совсем понимаю. :)
                              Потому что, как мне кажется, в контексте того, о чем мы говорим (о расстановке пробелов), конкретные имена не важны. Длина важна.

                                0
                                Так выше как раз пример со средней длины названиями, глядя на которые и расстановку пробелов не сразу понимаешь, где название функции. А если у нее название ещё длинее, а аргументы в скобках короткие или короткий и длинный?
                                  0

                                  Вы про имена с подчеркиваниями?

                                    0
                                    Можно и в другом стилем написать, без подчеркиваний, с большими-маленькими.
                                    result = firstLongLongArgument * longLongFunctionName (local, local)
                                      0

                                      Я не вижу здесь какой-то особенной проблемы с интерпретацией этого выражения.


                                      Однако, насколько я вас понимаю, вы пытаетесь сказать мне, что существуют ситуации, когда использование пробелов для разделения идентификаторов может привести привести к такому затруднению.


                                      Если так, то я готов с вами согласиться. Наверно, действительно, существуют такие случаи. Те примеры, которые вы приводили, однако, на мой взгляд, к ним не относятся.


                                      Пробелы облегчают нам чтение идентификаторов, но они же используются для формирования визуальной структуры программы. В случае сложных синтаксических конструкций эти две их функции вполне вероятно могут конфликтовать. В таком случае приходится чем-то жертвовать.


                                      Но это, как мне представляется, достаточно редкие случаи. Пока все что приходит в голову, это шаблоны. Наверно, будет неестественно выглядить, если разделить пробелами имя и описание типа в угловых скобках. Просто потому, что тип воспринимается как продолжение имени.

                0

                Не упомянут и почему-то редко используется вариант, когда пробелами выделяется список аргументов:


                result = someFunction( firstArgument, secondArgument );

                Здесь и скобка остаётся у имени функции, и разделены идентификаторы, и акцентировано завершение списка аргументов.
                Или ещё из примера со вложенными скобками, где, кстати, завершающая была потеряна:


                width = CFStringGetDoubleValue( (CFStringRef)CFArrayGetValueAtIndex(arrayRef,0) );

                Для вложенных вызовов, коротких или единственных аргументов выделение пробелами можно не использовать. А также и для синтаксических конструкций, чтоб вне зависимости от подсветки различать вызов и условие:


                whole( yards() );
                while (nine())
                  0
                  Есть в этом что-то. Хотя действительно редко встречается. И выглядит непривычно соответственно. Рука тянется вернуть правильное безобразие.
                    0
                    Хороший вариант. Только не соответствует литературным привычкам.
                    Я бы его и выбрал. Кстати, раньше так и писал.
                      0

                      Наверно, такой вариант тоже возможен. Его основной недостаток, на мой взгляд, что он не соответсвует привычкам из естественного языка. Кроме того, на мой взгляд, все-таки скобки ассоциируются со списком аргументов. То есть у нас есть имя функции и список аргументов, ограниченный скобками. Пробел в конце выглядит ненужным, его основная функция придать симметрию выражению в скобках.
                      В общем, неестественно.
                      Ну и последний аргумент мне тоже кажется надуманным.

                        0

                        Вот ещё момент.


                        result = someFunction (firstArgument, secondArgument);

                        Здесь пробел разделяет имя функции и список аргументов, то есть подчеркивает структуру этого выражения, то, что функции передается список аргументов. То есть у нас есть два объекта: функция, представляемая её именем, и жестко (за счет прилегающих скобок) связанный список аргументов.


                        result = someFunction( firstArgument, secondArgument );

                        А здесь скобка сильнее связана с именем функции, и получается, что фунция получает на список, а отдельные аргументы, и даже как бы не получает, а содержит их что ли. За счет пробелов, формируется ощущение некоторой свободы у аргументов, кажется, что они разбредутся, как овцы в загоне. Список разваливается на отдельные аргументы.

                          0

                          Возможны разные случаи сочетания имени функции и списка аргументов. В примерах рассматривается C++ — тогда это объявление, определение и вызов. Для объявления и определения отделение имени функции от скобки выглядит, пожалуй, оправданно:


                          private void someFunction (Type1 firstArgument, Type2 secondArgument);
                          ...
                          void SomeClass::someFunction (Type1 firstArgument, Type2 secondArgument)
                          {

                          А для вызова, когда параметры сами могут содержать скобки, кажется более важным именно выделить каждое передаваемое значение.


                          width1 = CFStringGet((CFStringRef)first(arrayRef,0), second, third(1));
                          width2 = CFStringGet ((CFStringRef)first(arrayRef,0), second, third(2));
                          width3 = CFStringGet( (CFStringRef)first(arrayRef,0), second, third(3) );

                          Разумеется, отличия в синтаксисе других языков будут влиять на рекомендации, направленные на улучшение зрительного восприятия.
                          Где-то (например, в Go) не требуются скобки в управляющих конструкциях. В Perl скобки не обязательны даже при вызове функций; это и к вопросу о списках и отдельных аргументах. Вместо круглых скобок может использоваться вертикальная черта (Rust). Во многих языках активно используются угловые скобки, часто вложенные.
                          И далеко не всегда общепринятый для какого-либо языка стиль написания кода соответствует оптимальному визуальному представлению. OTBS и camelCase побеждают.

                            0
                            На мой взгляд, второй подход и является более логичным. У нас не две отдельные сущности, функция и список аргументов, а одна целая сущность — функция. И функция «содержит» аргументы, как вы сказали. Это не какой-то список аргументов куда-то передаётся, а аргументы являются неотъемлимой частью именно этой функции, потому что без них это уже будет другая функция. Ведь функция определяется не именем, а сигнатурой.
                              0

                              Моя интерпретация этой синтаксической конструкции отличается от вашей: для меня это вызов функции, который состоит из имени функции и списка аргументов. Аргументы не принадлежат функции, а передаются ей.

                        +2
                        Да, наверно именно так и надо писать. Ссылаясь на исследования зрения. Иначе народ не понимает. Описаны те самые вещи которые пытался многим объяснять. Без исследований. Видно же с первого взгляда что так читать тяжело, а так легко. Ни один, никогда не согласился. Люди не видят что они видят. «А мне так удобно». Это как езда на велосипеде с полусогнутыми ногами — «удобно».
                          0

                          К сожалению, даже с результатами исследований, в большинстве случаев результат будет тот же.

                            0
                            Обсуждение смещается в область педагогики)
                          0
                          В Go дизайнеры языка просто запретили расставлять скобки разными способами, принесли удобочитаемость в жертву стандартизации.
                            0
                            image
                            Мне кажется, здесь вы так сконцетрировали внимание на идентификаторах, игнорируя исключительную важность скобок, что часть скобок растеряли. Я тоже не сразу заметил ошибку, строка воспринимается как набор идентификаторов, где-то там между ними затерялись скобки, что к чему относится, где имена функций, где аргуметы, непонятно.
                            Я сейчас используя такой стиль:
                            width = CFStringGetDoubleValue( (CFStringRef) CFArrayGetValueAtIndex( arrayRef, 0 ) )

                            Здесь сразу видно, где имя функции, потому что к нему привязана открывающая скобка списка аргументов. И в целом структура вложенности прослеживается чуть лучше, у вас же этот важный момент полностью игнорируется.
                              0

                              По поводу вашего стиля я уже отписывался выше: тут и тут.


                              В вашем варианте мне нравится то, что есть пробелы. В остальном нет. Почему, вы можете прочитать выше.


                              Ваши аргументы меня не убеждают, и в целом, на мой взгляд, выражение лучше не выглядит. Кажется каким-то рыхлым. Но спорить не буду. Тот стиль, который я привел, я использую достаточно давно, и он не вызывает у меня отторжения, хотя ничего идеального, конечно, нет. Попробуйте использовать свой стиль хотя бы несколько лет, возможно, вы не будете так категоричны.


                              Да, я писал про идентификаторы и про важность разделения их пробелами. Мне нужен был пример, но судя по всему, я подобрал его не совсем удачно, поскольку вы уже, как минимум, второй человек который пишет мне про структурированные параметры. Поэтому я его упростил: убрал ненужное приведение типа.


                              Ошибка со скобкой достаточно показательна. Во-первых, это хороший пример того, что надо использовать правильный инструмент для редактирования, который делает такие ошибки заметными. Чего я не делал по определенным причинам. Но это относится к редактированию, а не к чтению, и в данной статье не рассматривается. Во-вторых, интересно, что мне отсутствие скобки не мешало воспринимать этот фрагмент правильным образом. Но у меня были определенные ожидания. Сильно ли она помешала вам?

                                0
                                Мне отсутствие скобки несильно помешало, потому что мне и так не сразу понятно было, какие скобки куда относятся. Для меня, как видите, ваш вариант труднее воспринимать. Но восприятие текста гораздо менее важно, чем отсутствие ошибок. Первая задача стиля, как я это вижу, — способствовать минимизации ошибок, а уже потом можно думать над читабельностью.
                                P.S. Такую расстановку скобок я (и мои коллеги вслед за мной) использую около десяти лет, на работе. Я всегда в поиске более лучшего стиля, и время от времени что-то меняю, но к этим скобкам вопросов еще не возникало.
                                  0

                                  По поводу того, какой из вариантов труднее воспринимать, думаю смысла нет обсуждать. поскольку это, как я понимаю, уже достаточно высокоуровневая психическая деятельность, и она связана со сформированными привычками и определенными ментальными моделями (а у нас они по видимому отличаются).


                                  Но вот ваше утверждение:


                                  Но восприятие текста гораздо менее важно, чем отсутствие ошибок. Первая задача стиля, как я это вижу, — способствовать минимизации ошибок, а уже потом можно думать над читабельностью.

                                  — я принять не могу. Поскольку оно противоречит само себе.


                                  Невозможно обнаруживать и исправлять ошибки без понимания программы, а оно образуется только как результат восприятия её текста. Поэтому восприятие первично. Вы же обнаружили отсутствие скобки, только после того, как поняли, что это за выражение, не так ли? Вы можете утверждать, например, что ваш вариант легче для восприятия, и поэтому позволяет легче обнаруживать ошибки. И я (теоретически) могу с вами согласиться в этом. Но утверждать, что он позволяет находить ошибки, независимо от того, что может формировать текст трудный для восприятия, я думаю, вы всё-таки не станете.

                                  +1
                                  А в целом мне ваша статья понравилось, потому что я давно недоумеваю, почему никто не озадачивается таким немаловажным вопросом, как дизайн исходного кода. Как правильно писать текст на веб-страницах уже давно изучают в университетах, а исходный код до сих пор пишут кто на что горазд.

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое