бойлерплейт код что это
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# с использованием автоматических свойств с созданными компилятором резервными полями:
Шаблонный метод
В дополнение к объявлениям, методы в языках ООП также вносят свой вклад в количество шаблонных шаблонов.
Проведенное в 2015 году исследование популярных Java-проектов показывает, что 60% методов могут быть однозначно идентифицированы по появлению 4,6% его токенов, что делает оставшиеся 95,4% шаблонных неуместными для логики.
Исследователи полагают, что этот результат будет переведен в подпрограммы на процедурных языках в целом.
В HTML следующий шаблонный шаблон используется в качестве базового пустого шаблона и присутствует на большинстве веб-страниц:
Избавляемся от boilerplate кода в Protocol Buffers 2
В итоге получаем класс с таким интерфейсом:
Обратите внимание, что повсюду используются примитивы (что эффективно для сериализации и производительности). Но поле age у нас необязательное, но примитив всегда имеет дефолтное значение. Именно это и пораждает кучу boilerplate кода, с которым мы и будем бороться.
А ведь очень хочется написать:
Protocol Buffers имеет механизм расширения с помощью plugins, и его можно написать на Java, что мы и сделаем.
Что такое плагин для protobuf?
Это запускаемый файл, который читает из стандартного входящего потока объект PluginProtos.CodeGeneratorRequest, на его основе генерирует PluginProtos.CodeGeneratorResponse и записывает его в стандартный выходной поток.
Давайте рассмотрим подробнее, что мы можем сгененрировать?
PluginProtos.CodeGeneratorResponse содержит набор PluginProtos.CodeGeneratorResponse.File.
Каждый «file» — это новый класс, который мы генерируем самостоятельно. Он состоит из:
Самое важное для написания плагинов — мы не должны генерировать все классы заново — мы можем дополнять уже существующие классы используя insertionPoint. Если вернуться к сгенерированному интерфейсу выше — мы там увидим:
именно в эти места мы можем дописать свой код. Таким образом дописать в произвольный участок класса у нас не получится. От этого и будем отталкиваться. Как мы можем решить данную проблему? Мы можем сделать свой новый интерфейс с default методом —
а для класса Person добавить имплементацию не только PersonOrBuilder, но и PersonOptional
Теперь вернем из плагина код, который нужно сгенерировать
Как будем использовать наш новый плагин? — через maven, добавляем и настраиваем наш плагин:
Но можно и запустить его из консоли — здесь есть одна особенность запускать нужно не только наш плагин, а перед этим нужно вызвать стандарный java компилятор (но нужно создать исполняемый файл — protoc-gen-java8 (в моем случае просто bash-скрипт).
Исходный код можно посмотреть здесь.
Введение в 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 потребует огромного количества текста, поэтому мы рассмотрим наиболее релевантные. Однако имейте в виду, что существуют следующие классы:
Сравнение Java-записей, Lombok @Data и Kotlin data-классов
Несмотря на то что все три решения позволяют бороться с бойлерплейт кодом, общего между ними довольно мало. У записей более сильная семантика, из которой вытекают их важные преимущества. Что часто делает их лучшим выбором, хотя и не всегда.
… в одну строчку кода:
Конечно, аннотации @Data и @Value из Lombok обеспечивают аналогичную функциональность с давних пор, хоть и с чуть большим количеством строк:
А если вы знакомы с Kotlin, то знаете, что то же самое можно получить, используя data-класс:
Получается, что это одно и то же? Нет. Уменьшение бойлерплейт кода не является целью записей, это следствие их семантики.
К сожалению, этот момент часто упускается. Об уменьшении бойлерплейт кода говорят много, так как это очевидно и легко демонстрируется, но семантика и вытекающие из нее преимущества остаются незамеченными. Официальная документация не помогает — в ней тоже все описывается под углом бойлерплейта. И хотя JEP 395 лучше объясняет семантику, но из-за своего объема все довольно расплывчато, когда дело доходит до описания преимуществ записей. Поэтому я решил описать их в этой статье.
Семантика записей (records)
В JEP 395 говорится:
Записи (records) — это классы, которые действуют как прозрачные носители неизменяемых данных.
Таким образом, создавая запись, вы говорите компилятору, своим коллегам, всему миру, что указанный тип хранит данные. А точнее, иммутабельные (поверхностно) данные с прозрачным доступом. Это основная семантика — все остальное вытекает из нее.
Если такая семантика не применима к нужному вам типу, то не используйте записи. А если вы все равно будете их использовать (возможно, соблазнившись отсутствием бойлерплейта или потому что вы думаете, что записи эквивалентны @Data / @Value и data-классам), то только испортите свою архитектуру, и велики шансы, что это обернется против вас. Так что лучше так не делать.
(Извините за резкость, но я должен был это сказать.)
Прозрачность и ограничения
Давайте подробнее поговорим о прозрачности (transparency). По этому поводу у записей есть даже девиз (перефразированный из Project Amber):
API записей моделирует состояние, только состояние и ничего, кроме состояния.
Для реализации этого необходимы ряд ограничений:
для всех компонент должны быть аксессоры (методы доступа) с именем, совпадающим с именем компонента, и возвращающие такой же тип, как у компонента (иначе API не будет моделировать состояние)
должен быть конструктор с параметрами, которые соответствуют компонентам записи (так называемый канонический конструктор; иначе API не будет моделировать состояние)
не должно быть никаких дополнительных полей (иначе API не будет моделировать состояние)
не должно быть наследования классов (иначе API не будет моделировать состояние, так как некоторые данные могут находиться в другом месте за пределами записи)
И Lombok и data-классы Kotlin позволяют создавать дополнительные поля, а также приватные «компоненты» (в терминах записей Java, а Kotlin называет их параметрами первичного конструктора). Так почему же Java относится к этому так строго? Чтобы ответить на этот вопрос, нам понадобится вспомнить немного математики.
Математика
Итак, как вы поняли, тип — это множество, значения которого допустимы для данного типа. Это также означает, что теория множеств — «раздел математики, в котором изучаются общие свойства множеств» (как говорит Википедия), — связана с теорией типов — «академическим изучением систем типов» (аналогично), — на которую опирается проектирование языков программирования.
Это здорово, потому что теория множеств может многое сказать о применении функций к произведениям. Одним из аспектов этого является то, как функции, работающие с одним операндом, могут комбинироваться с функциями, работающими с несколькими операндами, и какие свойства функций (инъективные, биективные и т. д.) остаются нетронутыми.
В общем случае, чтобы применить теорию множеств к типу так, как я упоминал выше, ко всем его операндам должен быть доступ и должен существовать способ превратить кортеж операндов в экземпляр. Если верно и то и другое, то теория типов называет такой тип «тип-произведение» (а его экземпляры кортежами), и с ними можно делать несколько интересных вещей.
На самом деле записи лучше кортежей. В JEP 395 говорится:
Записи можно рассматривать как номинативные кортежи.
Следствия
Я хочу донести до вас следующую мысль: записи стремяться стать типом-произведением и, чтобы это работало, все их компоненты должны быть доступны. То есть не может быть скрытого состояния, и должен быть конструктор, принимающий все компоненты. Именно поэтому записи являются прозрачными носителями неизменяемых данных.
Итак, если подытожить:
Аксессоры (методы доступа) генерируются компилятором.
Мы не можем изменять их имена или возвращаемый тип.
Мы должны быть очень осторожны с их переопределением.
Компилятор генерирует канонический конструктор.
Преимущества записей
Большинство преимуществ, которые мы получаем от алгебраической структуры, связаны с тем, что аксессоры вместе с каноническим конструктором позволяют разбирать и пересоздавать экземпляры записей структурированным образом без потери информации.
Деструктурирующие паттерны
Благодаря полной прозрачности записей мы можем быть уверены, что не пропустим скрытое состояние. Это означает, что разница между range и возвращаемым экземпляром — это именно то, что вы видите: low и high меняются местами — не более того.
Блок with
И, как и раньше, мы можем рассчитывать на то, что newRange будет точно таким же, как и range за исключением low : нет скрытого состояния, которое мы не перенесли. И синтаксически здесь все просто:
выполнить блок with
передать переменные в канонический конструктор
(Обратите внимание, что этот функционал далек от реальности и может быть не реализован или быть значительно изменен.)
Сериализация
Для представления объекта в виде потока байт, JSON / XML-документа или в виде любого другого внешнего представления и обратной конвертации, требуется механизм разбивки объекта на его значения, а затем сборки этих значений снова вместе. И вы сразу же можете увидеть, как это просто и хорошо работает с записями. Они не только раскрывают все свое состояние и предлагают канонический конструктор, но и делают это структурированным образом, что делает использование Reflection API очень простым.
Более подробно том, как записи изменили сериализацию, слушайте в подкасте Inside Java Podcast, episode 14 (также в Spotify). Если вы предпочитаете короткие тексты, то читайте твит.
Бойлерплейт код
Вернемся на секунду к бойлерплейту. Как говорилось ранее, чтобы запись была типом-произведением, должны выполняться следующие условия:
аксессоры (методы доступа)
И все это генерируется компилятором (а также еще toString ) не столько для того, чтобы избавить нас от написания этого кода, сколько потому, что это естественное следствие алгебраической структуры.
Недостатки записей
Так что же делать, если вам все это нужно? Тогда записи вам не подходят и вместо них следует использовать обычный класс. Даже если изменив только 10% функциональности, вы получите 90% бойлерплейта, от которого вы бы избавились с помощью записей.
Преимущества Lombok @Data/@Value
Lombok просто генерирует код. У него нет семантики, поэтому у вас есть полная свобода в изменении класса. Конечно, вы не получите преимуществ более строгих гарантий, хотя в будущем Lombok, возможно, сможет генерировать деструктурные методы.
(При этом я не рекламирую Lombok. Он в значительной степени полагается на внутренние API компилятора, которые могут измениться в любой момент, а это означает, что проекты, использующие его, могут сломаться при любом незначительном обновлении Java. То, что он много делает для скрытия технического долга от своих пользователей, тоже не очень хорошо.)
Преимущества data-классов Kotlin
Вы часто создаете классы, основной целью которых является хранение данных. Обычно в таких классах некоторый стандартный и дополнительный функционал можно автоматически получить из данных.
Некоторые указывали на @JvmRecord в Kotlin как на большую ошибку: «Видите, data-классы могут быть записями — шах и мат ответ» (я перефразировал, но смысл был такой). Если у вас возникли такие же мысли, то я прошу вас остановиться и подумать на секунду. Что именно это дает вам?
Data-класс должен соблюдать все правила записи, а это значит, что он не может делать больше, чем запись. Но Kotlin все еще не понимает концепции прозрачных кортежей и не может сделать с @JvmRecord data-классом больше, чем с обычным data-классом. Таким образом, у вас есть свобода записей и гарантии data-классов данных — худшее из обоих миров.
В Kotlin нет большого смысла использовать JVM-записи, за исключением двух случаев:
перенос существующей Java-записи на Kotlin с сохранением ее ABI;
генерация атрибута класса записи с информацией о компоненте записи для класса Kotlin для последующего чтения каким-либо фреймворком, использующим Java reflection для анализа записей.
Рефлексия
Записи не лучше и не хуже рассмотренных альтернатив или других вариантов с аналогичным подходом, таких как case-классы Scala. У них действительно сильная семантика с твердым математическим фундаментом, которая хотя и ограничивает возможности по проектированию классов, но приносит мощные возможности, которые, в противном, случае были бы невозможны или, по крайней мере, не столь надежны.
Это компромисс между свободой разработчика и мощью языка. И я доволен этим компромиссом и с нетерпением жду, когда он полностью раскроет свой потенциал в будущем.
В преддверии старта курса «Java Developer. Professional» приглашаю всех желающих на бесплатный демоурок по теме: «Система получения курсов валют ЦБ РФ».
Пишите код, который легко удалять, а не дополнять
«Всякая строка кода рождается без причины, продолжается в слабости и удаляется случайно», — Жан-Поль Сартр программирует на 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-релизов от объединяемых ветвей или от основного кода. Возможность поменять свое решение прямо во время работы программы становиться все более важным в условиях, когда выпуск нового ПО может занимать часы, дни или недели. Спросите любого главного инженера по отказоустойчивости, и он скажет вам, что если система «будит» вас посреди ночи, значит она определенно должна предусматривать внесение изменений по ходу работы.
Речь в этом шаге идет не столько об итерациях как таковых, сколько о необходимости иметь петлю обратной связи. Не столько о написании многоразовых модулей, сколько о разделении компонентов для внесения в них изменений. Важно помнить, что введение изменений в основной код — это не только создание новых фич, но также и удаление старых. Писать расширяемый код — все равно что надеяться, что через три месяца с вашим проектом все будет хорошо. Написание кода, который вы сможете удалить — работа, основанная на противоположном предположении.
Стратегии, о которых я говорил выше по тексту — деление на уровни, изолирование, общие интерфейсы, композиция — призваны помочь вам не в том, чтобы написать хорошее ПО, но в том, чтобы создать такое ПО, которое способно меняться с течением времени.
«Вопрос управления, таким образом, заключается не в том, надо ли создавать пилотную систему и выбрасывать ее. Вы и так это сделаете.… Поэтому планируйте выбросить ее с самого начала; все равно так оно и получится». — Фред Брукс.
Конечно, это не значит, что вам надо выбрасывать абсолютно все, но некоторую часть удалить придется. Писать хороший код — не значит делать все правильно с первого раза. Хороший код — это просто унаследованный код, который не путается у вас под руками. И хороший код легко удалить.