12 июня 2018 г.

Configuration as code

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

Сейчас мне кажется, что выносить конфигурацию приложения в текстовые файлы (в том числе xml/json/yaml…) — это сильно переоцененный прием. Настолько переоцененный, что он уже почти антипаттерн. Ну как синглетон — сам по себе вроде неплох, но был период, когда его пользовали где ни попадя, в хвост и гриву. Так же и с конфигурацией в текстовых файлах: якобы это дает гибкость, но эта гибкость на практике оказывается чаще вредной, чем полезной.

И как раз когда я писал тесты для конфигурации — было очень заметно, насколько много в конфигурации лишней гибкости. Выясняется, что:
  1. Чтобы большая по объему конфигурация была управляемой — необходимы тесты
  2. При написании тестов большую часть времени я занимаюсь тем, что “отменяю” всю гибкость вольного формата текстового файла. Изобретаю заново — в тестах — те вещи, которые уже есть в строго-типизированном языке программирования.
Спрашивается: чего бы сразу не описывать конфигурацию на строго-типизированном языке?

Для меня текстовые конфигурации это наследство tutorial-ов, на которых программисты учатся. Когда я по ним учился, то там в обязательном порядке все, что можно, выносилось в .properties-файл. Но в tutorial-е в этот файл попадает 3-5 свойств, а к концу туториала — файлов становится аж 2, для UAT и для PROD. В реальном приложении к третьему году разработки таких файлов 150, свойств в них 10000, и всё горит, и все в аду, но это так привычно, что кажется just a normal amount of hell.

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

Это как с inversion of control/dependency-injection (очень близкая идея, кстати): ведь IoC/DI не равен тождественно spring-app-context.xml. Есть разные контейнеры, и разные форматы описания того, как контейнер должен собрать из компонентов приложение.

Можно и вообще без контейнера: когда я работал в 1С над Матконструктором, я, как продвинутый хипстер, конечно же сплетал компоненты в приложение через spring. Но у матконструктора была версия в виде апплета, и тащить мегабайт .jar-ников спринга в апплет только ради фазы инициализации было за гранью добра. Поэтому (я уже не помню, кому пришла в голову эта гениальная идея) мы написали xslt, которая из spring-контекста генерировала .java-файл, делающий ровно то, что было задекларировано в контексте. Это оказалось довольно просто сделать, так как мы пользовались только базовыми возможностями spring. И хотя в релизной версии приложения не оставалось никаких следов спринга, паттерн dependency injection сохранялся: сгенерированный класс был тем самым компонентом, ответственностью которого является собрать остальные компоненты в целое приложение.

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

Как я себе вижу конфигурацию в коде? Пока не очень четко :) Скорее всего, как DSL-like API для конфигурирования приложения. Java не самый лучший выбор для создания DSL-ей, но даже ее скупыми средствами вполне можно создать такой набор builder-ов, что многие ошибки конфигурации просто не пройдут компиляцию (а еще ведь остаются рантайм-проверки). А если взять какой-нибудь Котлин, то можно написать и вовсе гарные DSL-и. При этом:
  1. Есть проверка типов (у нас же строго типизированный язык!)
  2. Есть готовая вся система типов из доменной модели приложения, и ее можно расширить типами, специфичными для конфигурации.
  3. Есть поддержка IDE: подсвечивание ошибочных элементов, подсказки допустимых, всплывающие подсказки с описанием смысла параметров и их допустимых значений (если вы позаботились об этом при написании АПИ), переименование/рефакторинг
Поддержка IDE — это очень важный элемент, потому что он сильно сокращает время исправления ошибки. Например, есть немало решений для валидации конфигов, но валидация обнаруживает ошибки либо уже в рантайме, либо в тестировании, а ошибки компиляции современные IDE показывают сразу по ходу написания. А чтобы валидацию подхватила IDE — нужно постараться, и написать к ней специальный плагин, причем такой плагин нужен под каждый механизм валидации свой.

Вообще, мне кажется, сейчас эта идея витает в воздухе — возможно, потому, что появился Kotlin, который очень близко интегрируется с java, но при этом имеет отличные возможности для DSL. Вот пара идейно-близких примеров, которые я нашел сходу: Александр Тарасов рассказывает, как он использует kotlin-DSL чтобы конфигурировать эксперименты, а Иван Осипов — как они DSL-ем готовят параметры для параметризованных тестов.