HeadlinesBriefing favicon HeadlinesBriefing.com

Архитектура детерминированного ядра и оболочки

Hacker News •
×

Вчетыре недели назад Гари Бернхарит ввёл термин Functional Core, Imperative Shell. Как и большинство хороших идей в вычислениях, это было не совсем ново, но его концепция обладала великой ясностью и служит отличным основанием для разговора о тестировании и детерминизме в существующих системах. Кратко, архитектура Functional Core Imperative Shell делит код на две части. Functional Core является чисто функциональным - то есть без I/O и без разрушительных обновлений состояния. Он занимается бизнес-логикой приложения. Imperative Shell имеет меньше ветвлений, но сохраняет состояние, координирует внешние зависимости и общается с внешним миром - то есть с I/O. Его задача - запросить ядро по значениям, получить значения в качестве результата какого-то blackbox-решения, и использовать их для взаимодействия с внешним миром; будь то запись в базу данных, отправка запроса или обновление графического интерфейса. Ядро и оболочка в этой модели имеют отличительные характеристики: ядро оболочка принимает решения координирует зависимости много ветвящихся путей исполнения более линейное исполнение изолировано от мира интегрируется с миром Это делает ядро очень пригодным для тестирования. Поскольку оно чисто функционально, одинаковые входные данные всегда дают одинаковые результаты. Поскольку оно изолировано, нет ничего, что можно было бы подделать или заменить. И поскольку оно обрабатывает сложную бизнес-логику, тесты могут нам сказать многое о том, как ведёт себя система. Функциональная чистота и детерминизм Краткая формулировка свойств, которые делают чистые функции пригодными для тестирования, заключается в том, что они детерминированы. То есть - при заданном потоке входов, чистая функция всегда возвращает один и тот же поток выходов; её поведение повторяемо. Но чистое функциональное программирование - не единственный путь к этому. Если слегка наклонить голову, мы можем увидеть, что поток значений и последовательность присваиваний - это разные способы выражения одного и того же, и конечные автоматы могут принести нам те же преимущества. Рассмотрим следующий код: function add(ns) {return ns.reduce((a, b) => a + b, 0)} class Add Machine {#state = 0 transition(input) {this.#state += input} get state() {return this.#state}} функция add легко понимается; она чиста и поэтому детерминирована. Но и Add Machine также детерминирована - при одинаковой последовательности вызовов функции transition, Add Machine вернёт то же самое состояние. Ее императивность не меняет этого. const output = add([1, 2, 3]) const a = new Add Machine() a.transition(1) a.transition(2) a.transition(3) const output = a.state Чистое функциональное программирование - хороший парадигма, но из-за соображений языка или производительности оно не всегда практично - я бы не хотел попробовать это на C! Однако ослабление требований от чистой функциональности до всего лишь детерминированности позволяет сохранить преимущества тестируемости Functional Core Imperative Shell, расширяя при этом его применимость. Итак, название этой статьи: Детерминированное ядро, недетерминированная оболочка. Детерминизм может показаться более абстрактным понятием по сравнению с функциональной чистотой. Как вы это узнаёте? Я нахожу, что проще начать с того, что не является детерминированным, и работать назад. Вот несколько распространённых примеров неповторяемого поведения: Вызов генераторов случайных чисел, которые не инициализированы seed-ом Асинхронные и многопоточные операции Сетевое взаимодействие Взаимодействие с другими процессами Чтение Запись во внутреннее хранилище Взаимодействие с базами данных Запрос у операционной системы даты или времени Все это относится к недетерминированной оболочке. Когда вы находите их в своей бизнес-логике, у вас есть естественная цель для фрагментации - либо разделите функцию на две части вокруг неё, либо поднимите их на один уровень выше и внедрите их результат в качестве параметра. Иллюстративно р