Что такое boilerplate code
Проект Lombok, или Объявляем войну бойлерплейту
Подключаем Lombok
Lombok использует механизм процессинга аннотаций из Java 6, из чего следуют его минимальные требования к окружению. Для подключения Lombok к проекту достаточно включить его в зависимости. В случае использования Maven это делается следующим образом:
Для большей части функциональности Lombok эта библиотека необходима только на этапе компиляции. Последняя версия Lombok на текущий момент (0.11.0) ещё не попала в центральный репозиторий Maven, однако её без проблем можно установить в локальный или корпоративный репозиторий, скачав с сайта.
Прощаемся с аксессорами
Одним из главных источников бойлерплейта в Java является отсутствие свойств на уровне языка. Во соблюдение принципов ООП на каждое объявление поля приходится писать как минимум шесть типовых строк — геттер и сеттер. Некоторые библиотеки, вроде Spring или Tapestry, для своих целей в некоторых случаях позволяют разработчику забыть про аксессоры, вставляя их самостоятельно в байт-код. Подобную функциональность предлагает и Lombok.
Параметр lazy=true аннотации Getter позволяет реализовать ленивую инициализацию поля: вызов метода initMap() в данном случае будет отложен до первого вызова геттера и обёрнут в потокобезопасную инициализацию в виде блокировки с двойной проверкой.
Деструкция конструкторов
Конструкторы POJO-классов тоже не отличаются сложностью и разнообразием — чаще всего нам необходимо что-либо из этого списка: конструктор без параметров, конструктор со всеми параметрами, конструктор только с некоторыми обязательными параметрами, статический factory-метод. Lombok легко справляется с этой задачей с помощью аннотаций @NoArgsConstructor, @AllArgsConstructor, @RequiredArgsConstructor и параметра staticName соответственно.
Вот что мы получим в результате:
Генерируем типовые методы: toString, hashCode, equals
О правильной реализации методов equals и hashCode написано достаточно много — пожалуй, стоит по этому поводу вспомнить «Effective Java» Блока и статью Одерски. Вкратце можно сказать, что реализовать их корректно — непросто, поддерживать в актуальном состоянии — ещё сложнее, а занимать они вполне могут добрую половину класса. Метод toString не так критичен для корректности кода, но актуализировать его каждый раз при изменении класса — тоже приятного мало. Предоставим возможность Lombok сделать за нас эту неблагодарную работу с помощью двух нехитрых аннотаций:
Логгер-невидимка
Если вы пользуетесь одной из популярных библиотек протоколирования, то, вероятнее всего, в каждом классе у вас присутствует статическое объявление логгера:
Вместо этого Lombok предлагает воспользоваться аннотациями Log, @CommonsLog, @Log4j или @Slf4j — в зависимости от предпочитаемого средства протоколирования:
Финализируем локальные переменные
Хорошим стилем программирования является использование финальных локальных переменных, однако, учитывая статическую типизацию и отсутствие в Java выведения типов, объявление какой-нибудь особенно навороченной карты вполне может вылезть за пределы экрана. Если с Java переходить на Scala или Groovy пока не хочется, то можно воспользоваться следующим хаком Lombok:
Переменная map в данном случае будет объявлена как final, в то же время описание её типа будет взято из правой части выражения присваивания.
Безнаказанно бросаем исключения
Далеко не все разработчики, к сожалению, читали уже упоминавшуюся здесь «Effective Java» Блока или «Robust Java» Стелтинга. А может, читали, но не очень внимательно. Или, возможно, у них и впрямь была какая-то вполне обоснованная мотивация объявить вот это конкретное исключение проверяемым — но вам от этого не легче, ведь вы-то знаете, что оно не возникнет никогда! Что делать, например, с UnsupportedEncodingException, если вы абсолютно уверены, что без системной поддержки кодировки UTF-8 ваше приложение всё равно работать не будет? Приходится заключать код в try-catch и писать бессмысленный вывод в лог, который никогда не дождётся своего часа, переоборачивать исключение в runtime-обёртку, которой не суждено появиться на свет, или и вовсе игнорировать пустым блоком перехвата (который, не знаю, как у вас, а у меня лично всегда вызывает желание схватиться за револьвер). Lombok и здесь предлагает альтернативу.
Для этого колдунства, в отличие от всего прочего, понадобится подключить Lombok в рантайме. Всё просто: Lombok усыпляет бдительность компилятора, перехватывая исключение в try, а затем в рантайме незаметно перевыбрасывает его в catch. Фишка в том, что на уровне байт-кода можно выбросить любое исключение, даже то, которое не объявлено в сигнатуре метода.
Правильная синхронизация
Многопоточность — весьма сложная область программирования, имеющая в Java свою идиоматику и паттерны. Одной из правильных практик является использование для синхронизации приватных финальных полей, так как на любом общедоступном локе может пожелать синхронизироваться какой-либо не связанный кусок функциональности, что приведёт к ненужным блокировкам и трудно отлавливаемым ошибкам. Lombok умеет корректно синхронизировать содержимое метода, отмеченного аннотацией @Synchronized — как статического, так и метода экземпляра:
А что на это скажет IDE?
Все эти замечательные фишки ничего бы не стоили, если бы при открытии проекта в Eclipse или IntelliJ IDEA строчки кода разгорались бы красным пламенем от праведного гнева компилятора. К счастью, интеграция со средами разработки имеется, и достаточно неплохая. Для IntelliJ IDEA плагин присутствует в стандартном репозитории:
Для Eclipse и NetBeans установка немного необычная. Необходимо запустить файл lombok.jar, и он покажет симпатичный инсталлятор, предлагающий накатить Lombok на существующие инсталляции Eclipse:
Указанные плагины не только убеждают среду разработки в том, что отсутствие геттеров, сеттеров и прочих элементов бойлерплейта ей только чудится — они ещё и корректно подсвечивают ошибочные ситуации, например, когда геттер, которому надлежит быть сгенерированным, уже присутствует в классе:
Я перечислил основные фишки Lombok, однако это ещё не всё. Более подробно все возможные аннотации со всеми атрибутами и со сравнением кода «до» и «после» описаны в документации.
Введение в HTML5 Boilerplate
Даже для самых опытных ветеранов-дизайнеров начинать работу с новым стандартом может быть непросто, даже если язык разработан для упрощения кодирования, например HTML5. Говоря об этом, глубокое знание HTML 4.01 — это то, что может быть построено на основе, это просто случай, чтобы узнать, какие теги есть, а какие нет.
HTML5 Boilerplate помогает дизайнерам начать работу с новым стандартом, предлагая профессиональный интерфейсный шаблон, который позволяет создавать быстрый, надежный и адаптируемый сайт с набором HTML5-готовых функций и элементов.
Он был создан Paul Irish и Divya Manian и представляет собой проект с открытым исходным кодом, который идеально подходит для создания кросс-браузерных сайтов, которые работают со старыми браузерами, в то же время будучи готовыми для HTML5.
Его можно загрузить с веб-сайта HTML5 Boilerplate в его полной форме или в урезанной версии, которая не включает всю пояснительную документацию. Более легкая версия рекомендуется для тех, кто работал с ней раньше, и ее можно использовать, когда вы ознакомитесь с шаблоном. Для тех из вас, кто уверен, что вы можете работать с ним полностью, есть также настраиваемая опция, которая позволяет вам выбирать нужные элементы.
Что в коробке?
Основные функции, которые можно найти в HTML5 Boilerplate, включают в себя все необходимые элементы, которые вам понадобятся для начала, а также сопроводительную документацию:
Modernizr также включен для того, чтобы позволить вам стилизовать новые элементы HTML5 в IE, и помогает обнаруживать функции HTML5 или CSS3 во всех браузерах, включая более ранние версии IE (до v9).
Давайте сначала посмотрим на HTML, который по своей сути состоит из ряда условных комментариев IE для соответствующих IE-специфичных классов и CSS для более старых версий IE. Они дают определенное количество преимуществ для дизайнера, использующего технику условных классов, например, более простую интеграцию с CMS, такими как WordPress и Drupal.
Также исправлено множество CSS для более старых версий IE, а также классов IE, которые применяются к тег. Это проверяется как HTML5, а класс no-js HTML также включает в себя те же элементы, что и Modernizr и Dojo, а также параметры для Google Frame, Google CDN для jQuery и код отслеживания Google Analytics в области содержимого.
Вы также можете использовать HTML5 Boilerplate с Initializr ( ознакомьтесь с демонстрационной страницей здесь ), который генерирует шаблоны на основе Boilerplate, которые позволяют выбирать элементы, которые вы хотите, и те, которые вам не нужны. При этом у вас также есть возможность использовать адаптивный шаблон с самого начала, а не начинать с пустой страницы.
Начиная
После того, как вы загрузили HTML5 Boilerplate, пришло время настроить базовую структуру сайта, добавить контент, стиль и функциональность и начать тестирование! Вначале базовая структура будет выглядеть примерно так:
Итак, давайте посмотрим, как их можно использовать, начиная с CSS, каталог, который должен содержать все файлы CSS вашего сайта. Обратите внимание, что этот каталог уже содержит некоторые CSS, чтобы помочь вам начать работу, а также нормализовать CSS.
Стартовый CSS включает в себя:
Это не зависит от условных имен классов, таблиц условных стилей или Modernizr и может использоваться «из коробки» независимо от ваших предпочтений при разработке.
Общие помощники
Детализация фрагментов для всех классов CSS потребует огромного количества текста, поэтому мы рассмотрим наиболее релевантные. Однако имейте в виду, что существуют следующие классы:
Boilerplate code
При использовании языков, которые считаются многословными, программист должен написать много кода, чтобы выполнить только незначительную функциональность.
Такой код называется шаблонным.
Потребность в шаблонном коде может быть уменьшена с помощью высокоуровневых механизмов, таких как:
Происхождение
Родственным термином является бухгалтерский код, относящийся к коду, который не является частью бизнес-логики, но перемежается с ней для того, чтобы обновлять структуры данных или обрабатывать вторичные аспекты программы.
Преамбула
Одна из форм шаблонного кода состоит из объявлений, которые, хотя и не являются частью логики программы или основного синтаксиса языка, добавляются в начало исходного файла как обычай. Следующий пример Perl демонстрирует шаблонный шаблон:
#!/usr/bin/perl use warnings; use strict;
Первая строка-это shebang, который идентифицирует файл как Perl-скрипт, который может быть выполнен непосредственно в командной строке (в системах Unix/Linux.)
Два других-это прагмы (директивы), включающие предупреждения и строгий режим, которые предписаны модным стилем программирования Perl.
Следующий пример-шаблонный шаблон языка программирования C/C++, #include guard.
Это проверяет и устанавливает глобальный флаг, чтобы сообщить компилятору, является ли файл myinterface.h уже был включен.
Поскольку в компиляции модуля может быть задействовано много взаимозаменяемых файлов, это позволяет избежать многократной обработки одного и того же заголовка (что привело бы к ошибкам из-за нескольких определений с одним и тем же именем).
В объектно-ориентированном программировании
В объектно-ориентированных программах классы часто снабжаются методами получения и установки переменных экземпляра.
Определения этих методов часто можно рассматривать как шаблонные.
Хотя код будет варьироваться от одного класса к другому, он достаточно стереотипен по структуре, чтобы его лучше генерировать автоматически, чем писать от руки.
Большая часть шаблона в этом примере существует для обеспечения инкапсуляции.
Если бы имя переменных и владелец были объявлены общедоступными, методы доступа и мутатора не были бы нужны.
Чтобы уменьшить количество шаблонных шаблонов, было разработано много фреймворков, например Lombok для Java.
Тот же код, что и выше, автоматически генерируется фреймворком Lombok с использованием аннотаций Java, что является формой метапрограммирования:
@AllArgsConstructor @Getter @Setter public class Pet
В некоторых других языках программирования можно добиться того же с меньшим шаблоном, когда язык имеет встроенную поддержку таких общих конструкций.
Например, эквивалент приведенного выше кода Java может быть выражен в Scala, используя только одну строку кода:
case class Pet(var name: String, var owner: Person)
Или в C# с использованием автоматических свойств с созданными компилятором резервными полями:
Шаблонный метод
В дополнение к объявлениям, методы в языках ООП также вносят свой вкладhttps://sprutio.beget.com/# в количество шаблонных шаблонов.
Проведенное в 2015 году исследование популярных Java-проектов показывает, что 60% методов могут быть однозначно идентифицированы по появлению 4,6% его токенов, что делает оставшиеся 95,4% шаблонных неуместными для логики.
Исследователи полагают, что этот результат будет переведен в подпрограммы на процедурных языках в целом.
В HTML следующий шаблонный шаблон используется в качестве базового пустого шаблона и присутствует на большинстве веб-страниц:
Пишите код, который легко удалять, а не дополнять
«Всякая строка кода рождается без причины, продолжается в слабости и удаляется случайно», — Жан-Поль Сартр программирует на ANSI C.
Каждая новая строка кода приносит с собой затраты в виде необходимости ее поддержки. Чтобы избежать подобных затрат на работу с большим количеством кода мы прибегаем к его повторному использованию. Недостаток применения этого метода заключается в том, что он начинает мешать нам, в случае если мы захотим что-либо поменять в будущем.
Чем больше у вашего API пользователей, тем больше кода приходится переписывать для введения новых изменений. Верно и обратное: чем больше вы полагаетесь на сторонний API, тем больше проблем испытываете когда он изменяется. Упорядочивание взаимодействия и взаимосвязей разных частей кода является серьезной проблемой в больших системах. И по мере развития проекта, растет и масштаб этой проблемы.
Перевод статьи на русский язык подготовлен компанией PayOnline, провайдером платежных решений для вашего онлайн-бизнеса.
Я говорю о том, что если уж мы так хотим считать количество строк кода, нам следует смотреть на них не как на «произведенные строки», но как на «потраченные строки», — Э. Дейкстра, рукопись 1036.
Если относиться к «строкам кода» как к «потраченным», тогда, удаляя их, мы снижаем стоимость поддержки. Вместо создания повторно используемых программ, нам следует стремиться к созданию программ одноразового употребления. Думаю, не нужно объяснять вам, что удалять код гораздо веселее, чем писать его.
Чтобы написать легко поддающий удалению код, старайтесь всячески избегать зависимостей в общем и как можно чаще отказываться от их упорядочивания. Разбивайте свой код на уровни: пишите простые в использовании API, создавая их на основе более простых в применении, но в целом менее удобных по отдельности решений. Разделяйте код, изолируя сложные в написании и наиболее изменчивые части от остального программного кода и друг от друга. Не делайте жестких определений для всех возможных случаев: в некоторых ситуациях лучше оставить возможность выбора во время работы программы. Не пытайтесь заниматься всем этим одновременно и подумайте, следует ли вам вообще писать так много кода.
Шаг 0: Не пишите код
Количество строк кода само по себе мало о чем говорит, а вот эффект от 50, 500, 5 000, 10 000, 25 000 строк и т. д. отличается существенно. Монолит размером в миллион строк попортит вам больше нервов, чем структура в 10 тыс. строк. Если же говорить о времени, деньгах и усилиях, которые вам придется потратить на его замену, то здесь разница будет ощущаться гораздо сильнее.
Чем больше у вас кода, тем сложнее от него избавиться. Тем не менее, сохранение одной строчки кода не дает никаких результатов само по себе. Как бы то ни было, удалять проще всего такой код, от которого вы успели отказаться еще до того, как приняться за его написание.
Шаг 1: Пользуйтесь копипастой
Писать «многоразовый» код гораздо легче задним числом, имея на руках несколько примеров использования, нежели пытаться оценить, какие из них могут потребоваться в будущем. Впрочем, положительные стороны этого метода вы ощущаете на себе, уже просто работая с файловой системой. Поэтому с повторным использованием кода вроде бы все в порядке: небольшая избыточность пойдет программе только на пользу.
Пользоваться копипастой время от времени лучше, чем создавать библиотечную функцию, только лишь для того, чтобы понять, как эта функция будет себя вести в вашем случае. То есть следует хорошенько подумать, надо ли вам в данный момент писать функцию вместо копипасты, потому что как только вы превращаете свои наработки в общедоступный API, процесс их последующего изменения становится сложнее.
Всегда помните, что написанная вами функция будет вызываться как по прямому назначению, так и для других вещей, о которых вы даже и не думали в момент ее создания. Использующие ее программисты будут полагаться на собственные наблюдения, а не на то, что вы написали в документации. Ну и, конечно, легче будет удалить содержимое функции, нежели саму функцию.
Шаг 2: Не пользуйтесь копипастой
Небольшое отступление: создайте для util специальную директорию и сохраняйте каждую утилиту в отдельный файл. Использование одного util-файла приведет к тому, что он в конечном счете вырастет до огромных размеров и тогда делить его на отдельные части будет очень сложно. Всегда помните, что ведение одного util-файла — плохая практика.
Чем менее специфичен тот или иной код для вашего приложения или проекта, тем легче вам будет использовать его повторно, и тем меньше вероятность его изменения или удаления. Это, как правило, библиотечный код, описывающий запись данных, работу со сторонними API, дескрипторы файлов или процессы. Еще примеры, которые вам не нужно будет удалять — списки, хеш-таблицы и другие наборы данных. Не из-за частой простоты их интерфейсов, но потому, что они не будут расти с точки зрения области применения с течением времени.
Не пытайтесь специально облегчить себе задачу удаления кода. Вместо этого на данном этапе мы должны стараться держать трудно поддающиеся удалению части программы как можно дальше от частей, удалить которые легко.
Шаг 3: Пишите больше boilerplate-кода
Библиотеки пишутся, чтобы избежать постоянной копипасты. И тем не менее, часто получается так, что в процессе их написания мы добавляем еще больше копипасты, но называем ее по-другому: boilerplate. Создание бойлерплейтов во многом похоже на копипастинг, с той разницей, что в нем вы каждый раз меняете разную часть кода, а не одну и ту же. Как и в случае с копипастой, вы дублируете части кода, чтобы избежать представления зависимостей и добиться гибкости, взамен получая еще большую избыточность.
Библиотеки, которым требуются бойлерплейты — это часто разработки вроде сетевых протоколов, форматов передачи данных, инструментов для парсинга и вообще все те кодовые базы, в которых сложно объединить политики (то, что программа должна делать) с протоколом (что она может делать) без накладывания каких-либо ограничений. Такой код сложно удалить: как правило, он требуется для общения с другими компьютерами или обработки различных файлов. При этом загрязнять его бизнес-логикой — это последнее, чего мы хотим. Нет, речь не идет о каком-то дополнительном упражнении в повторном использовании кода. Мы просто стараемся держать все подверженные частым изменениям блоки кода подальше от относительно статических. То есть мы занимаемся минимизацией зависимостей библиотечного кода, хоть пусть нам и приходится писать для него бойлерплейт. В итоге получается, что вы пишите больше строк кода, но все они приходятся на те части, которые легко поддаются удалению.
Шаг 4: Не пишите boilerplate-код
Бойлерплейты лучше всего работают, когда предполагается, что библиотеки будут соответствовать самым разным вкусам разработчиков, но иногда такой подход приводит к чрезмерной избыточности. Тогда наступает время поместить вашу гибкую библиотеку внутрь другой, которая обладает своими взглядами на правила, схемы и состояния. Создание простых в использовании API заключается в превращении вашего boilerplate в библиотеку. И это не такая уж и редкость, как вы могли бы подумать. В качестве примера можно привести один из самых популярных и любимых http-клиентов для Python, requests, который успешно справляется с предоставлением простого интерфейса, работая на основе более избыточной библиотеки urllib3. Requests позволяет пользоваться многими типовыми схемами работы с http, скрывая большинство подробностей от глаз пользователя. В то же время urllib3 выполняет конвейеризацию, управление соединением и ничего от пользователя не прячет.
Дело здесь не столько в том, чтобы скрывать какие-то подробности, помещая одну библиотеку внутрь другой, сколько в разделении ответственности: requests можно сравнить с турагентством, которое дает на выбор путевки для популярных в мире http путешествий, тогда как urllib3 нужна, чтобы удостовериться, что у вас есть все необходимое, чтобы это путешествие прошло как надо.
Нет, я не призываю вас немедленно пойти и создать директории /protocol/ и /policy/. Однако, возможно, это станет необходимостью, ведь вы наверняка захотите содержать вашу util-директорию свободной от всякой бизнес-логики и при этом продолжать работать над созданием своего тандема библиотек. Вы вполне можете работать над ними параллельно, не дожидаясь, пока работа над базовой библиотекой будет завершена.
Часто бывает полезным также делать «обертки» для сторонних библиотек, даже если они выполнены в виде протокола. Вы можете создать библиотеку, подходящую именно для вашего кода, вместо того, чтобы использовать общие для всего проекта решения. Часто бывает так, что создаваемый вами API не может быть одновременно приятным в использовании и хорошо расширяемым. Эти два понятия идут вразрез друг с другом.
Разграничение ответственности позволяет нам порадовать некоторых пользователей, не перекрывая кислород другим. Деление на уровни легче всего делать, когда у вас изначально есть хороший API, однако написание хорошего API поверх плохого едва ли придется вам по вкусу. Хорошие API проектируются с учетом того, как их будут видеть пользователи (т. е. программисты), и создание иерархии в этом смысле означает понимание того, что вы не можете угодить всем одновременно.
Смысл разделения кода на уровни состоит не столько в том, чтобы написать код, который вы впоследствии сможете удалить, сколько в том, чтобы сделать трудноудаляемый код приятным в использовании (не загрязнять его бизнес-логикой).
Шаг 5: Напишите большой блок кода
Сколько бы вы ни копипастили, ни рефакторили, ни делили на уровни, ни проектировали, все сводится к тому, что код должен выполнять какую-то работу. Иногда, если все идет не так, как задумывалось, лучше всего просто сдаться и написать доброе количество низкокачественного кода, просто чтобы все остальное заработало.
Бизнес-логика — это бесконечная серия пограничных случаев, а также быстрых и грязных трюков. И это нормально. Меня это устраивает. Другие стили вроде «игрового кода» и «кода основателя» представляют собой то же самое: попытка срезать на поворотах, чтобы сэкономить значительное количество времени.
Почему я предлагаю просто взять и написать много кода? Потому что избавляться от одной большой ошибки бывает гораздо легче, нежели пытаться удалить 18 маленьких, тесно переплетенных друг с другом. Программирование вообще во многом связано с исследованием. Сделать несколько ошибок и получить результат — способ более быстрый, чем пытаться продумать все с первого раза. Это особенно справедливо в случаях веселых или творческих начинаний. Если вы пишете свою первую игру, не начинайте этот процесс с движка. Аналогично этому, не пишите веб-фреймворк поперек приложения. Я говорю так, потому что знаю, что у вас все равно получится бардак, разобраться в котором не сможет ни один психически здоровый человек. Поэтому лучше сядьте и напишите сначала именно это бардак.
Монорепозитории представляют собой такой же компромисс: вы не будете заранее знать, как разделить код. Ну а разворачивать такую вот «одну большую ошибку» легче, чем 20 тесно связанных. Когда вы знаете, какую часть кода надо будет вскоре забросить, а какую удалить или с легкостью заменить, вы можете срезать гораздо больше углов. Так бывает, когда вы занимаетесь заказами по сайтам и веб-страницами, посвященными одноразовым событиям, ну или любой подобной работой, где у вас есть готовый шаблон и все что вам остается делать — это штамповать копии или просто заполнять пробелы, оставленные разработчиками фреймворка.
Нет, я не предлагаю вам писать одну и ту же ерунду по десять раз, пытаясь исправить все ее ошибки. Я говорю о другом. Как сказал когда-то Алан Перлис: «Все должно создаваться сверху вниз, за исключением первого раза». Не бойтесь совершать новые ошибки, брать на себя новые риски и пусть медленно, но верно, с помощью итерации продвигаться вперед.
Стать профессиональным разработчиком ПО означает собрать целый каталог сожалений и ошибок. Успех ничему не учит. Вы не можете заранее знать, как выглядит хороший код, но вот оставшиеся от плохого кода шрамы всегда свежи в вашем сознании. В любом случае проекты, в конце концов, либо терпят неудачу, либо становятся унаследованным кодом. Неудачи случаются чаще, чем успехи. Быстрее будет слепить десять разных комков грязи и посмотреть, что из этого получится, нежели пытаться довести до блеска одну кучу дерьма. Удалять код целиком проще, чем делать это по частям.
Шаг 6: Делите код на части
Большие комки грязи легко лепить, но вот поддерживать — сложнее всего. Попытка внести в них простое, на первый взгляд, изменение заканчивается внесением исправлений почти во все части кодовой базы.
Итак, мы создали в нашем коде иерархию для разделения ответственности как для платформенных, так и для доменных задач, и теперь нам надо найти способ разделить логику, которая находиться поверх всего этого.
«Начните со списка самых сложных проектных решений или тех из них, которые с наибольшей вероятностью изменятся. Далее проектируйте каждый модуль так, чтобы скрыть такое решение от других модулей». — Дэвид Парнас.
Вместо того чтобы разбивать код на части со схожей функциональностью, мы делим его на части, исходя из того, чем они отличаются друг от друга. Мы выделяем те из них, которые вызывают наибольшие трудности в написании, поддержке или удалении. Мы не создаем модули, исходя из того, сможем ли мы использовать их повторно, главное, чтобы их было удобно менять в будущем.
К сожалению, некоторые проблемы оказываются связанны друг с другом теснее, и их бывает сложнее отделить от других. Несмотря на принцип одной ответственности, который гласит, что «каждый модуль должен решать только одну сложную проблему», на деле гораздо важнее, чтобы «решением каждой сложной проблемы занимался только один модуль». В случаях, когда модуль занимается сразу двумя вещами, это происходит, потому что изменение одной части требует изменения другой. Работать с одним ужасным, но простым в плане интерфейса компонентом часто бывает проще, чем с двумя компонентами, требующими тщательной координации друг с другом.
«Я не стану пытаться сейчас точнее определить материал, подпадающий под это краткое описание [«слабая связанность»]; возможно, я никогда не сумею дать этому внятное определение. Однако я знаю, когда вижу, и кодовая база, рассматриваемая в этом деле, не такая». — Судья Верховного суда США Стьюарт.
Система, в которой вы можете удалять те или иные ее части без необходимости переписывания другие, часто называется слабосвязанной, однако объяснить как она выглядит на деле гораздо проще, чем заранее знать как ее создать. Слабая связанность допускает даже жесткое задание переменной или использование флага командной строки поверх нее. Смысл этой методики заключается в получении возможности менять основные решения без необходимости переделывать весь код.
В продуктах Microsoft Windows, к примеру, для этих целей используются внешние и внутренние API. Внешние API привязаны к жизненному циклу десктопных программ, а внутренний — к ядру, на основе которого они работают. Скрытие этих API дает Microsoft гибкие возможности по внесению в систему изменений без какой-либо опасности сломать кучу программ в результате этой деятельности.
HTTP также содержит примеры слабой связанности: добавление кэша перед HTTP сервером, перемещение изображений в CDN, в результате которого изменяются лишь ссылки на них. Ни тот, ни другой механизм не ломают ваш браузер. Другой пример слабой связанности — применяемые в HTTP коды ошибок. Общие для серверов по всему вебу проблемы имеют свой уникальный идентификатор. Когда вы получаете 400 ошибку, вы знаете, что выполнение тех же операции, которые к ней привели, никак не изменит ситуацию. А вот в случае с 500 ошибкой повторная перезагрузка страницы может все изменить. HTTP-клиенты могут обрабатывать множество ошибок, избавляя программистов от необходимости делать это самостоятельно.
Следует учитывать, как ваше ПО будет обрабатывать ошибки, когда вы будете раскладывать его на более малые части. И, конечно же, об этом тоже легче говорить, чем делать.
«Я решил, хоть и с большой неохотой, использовать LATEX».— Джоуи Армстронг. Создание распределенных систем, надежно работающих при наличии в них программных ошибок. 2003 г.
Erlang/OTP весьма уникален в плане способа обработки ошибок, который называется «деревья контроля». Говоря в общих чертах, каждых процесс в Эрланг-системах запускается и наблюдается супервизором. Когда процесс сталкивается с проблемой, он прекращает свою работу, после чего сразу же перезапускается супервизором. Что же касается самих супервизоров, то они запускаются начальным процессом, который также осуществляет их перезапуск, когда со сбоем сталкиваются уже они.
Ключевая идея состоит в том, что работа по схеме «ошибка-перезапуск» проходит быстрее по сравнению с попытками обработки ошибок. Подобное обращение со сбоями, когда надежность достигается путем отказа от решения возникшей проблемы, может показаться контринтуитивным, однако на практике метод выключения и перезапуска оказывается очень эффективным в деле подавления разовых и преходящих сбоев.
Обработка ошибок и восстановление лучше всего выполнять на внешних уровнях вашей кодовой базы. По-другому это называется принципом взаимодействий двух оконечностей. Он гласит, что обрабатывать ошибки легче на двух дальних концах связующей среды, нежели где-либо в ее середине. Это связано с тем, что даже если работа над ошибкой происходит где-то посередине, вам, так или иначе, придется делать проверку и на пограничных уровнях. Если каждому верхнему уровню все равно придется обрабатывать ошибки, то зачем же тогда делать это еще и где-то внутри программы?
Обработка ошибок — один из множества способов, с помощью которых система может оказаться тесно связанной внутри. Существует немало других примеров тесной связанности, и все же выделять какой-то один в качестве плохого было бы нечестно. Кроме IMAP.
В IMAP практически каждая операция, словно снежинка, обладает уникальными параметрами и алгоритмом обращением. Обработка ошибок становится очень неприятным процессом: ошибки могут появиться прямо посередине выполнения другой операции.
Вместо UUIDs, IMAP генерирует уникальный токен для идентификации каждого сообщения. Последний также может измениться прямо во время выполнения другой операции. Многие операции можно поделить на части. Потребовалось 25 лет для изобретения способа, который позволял бы надежно перемещать электронные письма из одной папки в другую. И, конечно, нельзя не отметить применения в нем весьма специфичных кодировок UTF-7 и base64.Нет, я ничего не выдумываю.
Для сравнения: как файловая система, так и база данных представляют собой гораздо лучшие примеры удаленного хранилища. Файловая система предлагает фиксированный набор операций, однако набор объектов, над которыми вы можете их производить, велик и весьма разнообразен. Может показаться, что интерфейс SQL обладает более широкими по сравнению с файловой системой возможностями. Тем не менее, он использует ту же схему работы: есть некоторое количество операций для работы с набором данных и огромное количество строк, над которыми эти операции проводятся. И хотя вы не всегда можете сделать замену одной базы на другую, найти решения, которые работали бы с SQL гораздо проще, чем аналогичные решения для любого кустарного языка запросов.
В качестве других примеров слабой связанности можно привести системы, использующие межплатформенное ПО или фильтры и конвейеры. Finagle твиттера использует обычный API для сервисов, и это позволяет без каких-либо лишних усилий добавить базовую обработку таймаутов, механизмов повторного соединения и проверки подлинности. И, конечно, я не могу не упомянуть в связи с этим конвейер UNIX. Это вызвало бы большое негодование.
Итак, сначала мы разделили свой код на уровни, однако теперь уже некоторые из этих уровней вместе используют один интерфейс: некий общий набор поведений и операций, пригодный для самых разных вариантов применения. Хорошими примерами слабой связанности часто оказываются однородные интерфейсы.
Правильная кодовая база совсем необязательно должна быть идеально поделена на модули. Просто модульность делает процесс написания кода гораздо более интересным. Это как детали Lego, играть с которыми интересно, потому что они подходят друг к другу. Здоровая кодовая база всегда обладает небольшим избытком функциональности, а также ровно таким расстоянием между движущимися частями, чтобы ваши руки в них не застряли.
Слабосвязанный код совсем необязательно легко удалить, однако заменить его или внести в него изменения всегда значительно легче.
Шаг 7: Продолжайте писать код
Умение писать код без оглядки на ранее написанные строки серьезно облегчает процесс экспериментирования с новыми идеями. Нет, я не говорю, что вам теперь всегда нужно стараться писать микросервисы вместо монолитов, но ваша система должна позволять вам провести один или два эксперимента поверх вашей основной работы.
Feature flags — один из способов изменить принятые ранее решения. Несмотря на то, что feature flags многими воспринимаются как способ экспериментирования с новыми возможностями, они также позволяют вам добавлять изменения без повторного развертывания новой версии.
Google Chrome — потрясающий пример положительных моментов, которые они в себе несут. Разработчики Chrome поняли, что самым сложным моментом поддержки регулярного цикла релизов была большая трата времени на объединение существующих давно feature-ветвей.
Возможность включить или выключить новый код в любой момент без его повторной компиляции позволяет разбивать крупные изменения на более мелкие слияния без нанесения ущерба существующему основному коду. Кроме того, благодаря заблаговременному появлению новых фич в той же самой кодовой базе, команда получила возможность предвидеть ситуации, когда разработка долгоживущей фичи повлияет на другие части кода.
Feature flag — непросто параметр командной строки. Это способ разделения feature-релизов от объединяемых ветвей или от основного кода. Возможность поменять свое решение прямо во время работы программы становиться все более важным в условиях, когда выпуск нового ПО может занимать часы, дни или недели. Спросите любого главного инженера по отказоустойчивости, и он скажет вам, что если система «будит» вас посреди ночи, значит она определенно должна предусматривать внесение изменений по ходу работы.
Речь в этом шаге идет не столько об итерациях как таковых, сколько о необходимости иметь петлю обратной связи. Не столько о написании многоразовых модулей, сколько о разделении компонентов для внесения в них изменений. Важно помнить, что введение изменений в основной код — это не только создание новых фич, но также и удаление старых. Писать расширяемый код — все равно что надеяться, что через три месяца с вашим проектом все будет хорошо. Написание кода, который вы сможете удалить — работа, основанная на противоположном предположении.
Стратегии, о которых я говорил выше по тексту — деление на уровни, изолирование, общие интерфейсы, композиция — призваны помочь вам не в том, чтобы написать хорошее ПО, но в том, чтобы создать такое ПО, которое способно меняться с течением времени.
«Вопрос управления, таким образом, заключается не в том, надо ли создавать пилотную систему и выбрасывать ее. Вы и так это сделаете.… Поэтому планируйте выбросить ее с самого начала; все равно так оно и получится». — Фред Брукс.
Конечно, это не значит, что вам надо выбрасывать абсолютно все, но некоторую часть удалить придется. Писать хороший код — не значит делать все правильно с первого раза. Хороший код — это просто унаследованный код, который не путается у вас под руками. И хороший код легко удалить.