Машина Руба Голдберга
Машина Руба Голдберга

Рабочий код – только полдела

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

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

На самом деле, умение писать – это искусство четко доносить идеи.

 Большинство качеств хорошего эссе актуальны и для хорошего исходного кода.

  • Хорошие идеи.

  • Приятно читать (стили).

  • Хорошая организация идей (логичность изложения).

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

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

Что на самом деле понимается под разделением ответственности?

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

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

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

  1. Главы книги (общий план).

  2. Разделы.

  3. Абзацы.

  4. Высказывания (нижний уровень).

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

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

Итак, как же создаются организованные идеи? К этому есть несколько подходов:

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

  • Чертите деревья зависимостей и выявляйте оптимальные способы создания таких потоков и группирования идей.

  • Излагайте общие паттерны для моделирования ваших задач, четко опишите, как ваша задача вписывается в конкретный паттерн.

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

Что характерно для хорошо организованного кода:

  • Он понятен сразу на многих уровнях.

  • Его разделы имеют удобный размер.

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

Итак, что же большинство из нас понимает под «разделением ответственности»? На самом деле, при этом обычно понимается правильная организация зон ответственности (их разделение и группирование).

Как действует разделение ответственности?

«Разделение» в понятии «разделение ответственности» достигается при помощи правильной расстановки пробелов. Есть несколько способов это сделать. Вот как можно делить код: на блоки (подобные абзацам), на функции, перенести в другой файл, в другой каталог, в другой модуль, во внешнюю библиотеку, т.д.

Вот как можно разделить функцию на своеобразные абзацы.

processExpression(index, tokens) {
	  let factorOne = null;
	  let comparitor = null;
	  let factorTwo = null;
	

	  // Получаем первый коэффициент
	  let factor;
	  factor = this.getFactor(index, tokens);
	  factorOne = factor.value;
	  index = factor.newIndex;
	

	  // Получаем оператор сравнения
	  comparitor = tokens[index].value;
	  index++;
	

	  // Получаем второй коэффициент
	  factor = this.getFactor(index, tokens);
	  factorTwo = factor.value;
	  index = factor.newIndex;
	

	  // Получаем итог
	  return this.evaluateExpression(factorOne, comparitor, factorTwo)    
	}

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

Разрывы могут быть полезны в трех отношениях:

  • Код группируется на удобопонятные фрагменты (пошагово).

  • Расстановка «дорожных знаков», чтобы нужный код было легче найти (и проследить).

  • Можно обфусцировать тот код, который вдается в детали более, чем требуется.

Рассмотрим, как разрывы разного типа позволяют достигать этих целей.

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

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

Проделывая это с функцией processExpression, вы понимаете, что при этом:

  • Извлекается первый коэффициент.

  • Извлекается оператор сравнения.

  • Извлекается второй коэффициент.

  • С использованием всех трех частей вычисляется результат.

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

Аналогично, обфускация кода упрощает понимание программы, поскольку излагает «сюжеты» именно на той глубине проникновения в код, что нам нужно. Просмотрев код выше, вы видите, что функция this.evaluateExpression(factorOne, comparitor, factorTwo)evaluateExpression должна обрабатывать много сущностей в зависимости от типа ввода (числа, строки, булевы значения, т.д.).

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

Какие разрывы лучше – мягкие или жесткие?

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

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

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

Допустим, у вас есть компонент, глубина которого составляет 3 уровня. Меню, в меню есть запись, а из этой записи открывается панель инструментов. Данными владеет компонент «Menu», и все события в компонентах отражаются на этом родительском уровне данных. Теперь у каждого файла также есть свой собственный внешний CSS-файл. Это вполне обычный уровень сложности, но, чтобы не пришлось активно по нему перемещаться, потребуется одновременно открыть 6 окон с кодом. Следовательно, решаясь применять жесткие разрывы, постарайтесь обеспечить, чтобы разделенные таким образом файлы не были слишком тесно переплетены друг с другом (когда вы работаете в одном файле, остальные можно не открывать).

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

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

Я не хочу сказать, что жесткие разрывы – это плохо. Просто нужно тщательно взвешивать их достоинства и недостатки и расставлять разрывы с известной тщательностью.

Группирование зон ответственности

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

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

Размер групп

Тут все просто.

  • Параграф кода: 1–6 строк.

  • Функция: рекомендуется 5–30 строк. Максимум: ~100 строк.

  • Файл: рекомендуется 100–200 строк. Максимум: ~1000 строк.

  • Каталог: 7–8 подкаталогов, каждый ~7–8 файлов. (Очевидное исключение из этих правил задания размера касается каталогов с самыми базовыми блочными компонентами (кнопками, названиями, иконками, т.д.). Таких компонентов можно класть по 20-30 в каталог, и структура от этого не испортится.

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

Дискретные группы

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

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

  2. Близкие подзадачи остаются сгруппированы рядом друг с другом.

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

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

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

Резюме

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

Для разделения используются разрывы, позволяющие разбить код на:

  • Параграфы.

  • Функции/Классы/Структуры/т.д.

  • Отдельные файлы.

  • Отдельные каталоги.

  • Внешние библиотеки.

У такого разделения три основных достоинства:

  • Код группируется в виде небольших удобных для понимания шагов (каждая группа – один шаг).

  • Создается разметка наподобие дорожных знаков, благодаря которым код проще находить (и прослеживать).

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

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

  • Размер группы.

  • Дискретность группы.

Для придания нужного размера группам используются мягкие и жесткие разрывы. В крупных группах сложно ориентироваться. При жестких разрывах также может быть сложно ориентироваться в группах.

В конечном счете, наилучший способ оптимизировать разделение ответственности в коде – рефакторинг! А в следующий раз, когда начнете писать код, вспомните о том, что прочли в этой статье.

 

Комментарии (4)


  1. vtools
    21.06.2022 11:55

    Я так понимаю здесь какой-то неведомый мне юмор:

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

    А на самом деле:

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


    1. MentalBlood
      21.06.2022 13:56

      В оригинале obfuscation. obfuscation переводится еще как затемнение, так что никакого юмора, просто перевод такой. Хотя и в нем понятно о чем речь


  1. vtools
    21.06.2022 15:43
    -2

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


    1. ph_piter Автор
      21.06.2022 19:36
      +3

      Нет, позволим себе с вами не согласиться. У "Separation of concerns" есть устоявшийся перевод "разделение ответственности" https://ru.wikipedia.org/wiki/Разделение_ответственности, см. также авторский перевод Владимира Хорикова https://habr.com/ru/post/263027/