Русский
Русский
English
Статистика
Реклама

Архитектура приложений

Фрактальная шизофрения. Whats up?

01.04.2021 04:04:20 | Автор: admin


По некоторым источникам еще в IV до нашей эры Аристотель задался одним простым вопросом Что было раньше? Курица или яйцо? Сам он в итоге пришел к выводу, что и то, и другое появилось одновременно вот это поворот! Не правда ли?


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


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


Сегодня я не буду терзать ваш разум водопадом отборного бреда. Выпуск будет чуть суровее ибо лирики меньше, а кода больше. Тем не менее местами он будет вкуснее ибо вишенки я вам приготовил отменные. Поехали?


What's up guys?


Кажется моя лаборатория по производству генераторов-мутантов слепила что-то действительно годное.


npm i whatsup

Знакомьтесь фронтенд фреймворк вдохновленный идеями фракталов и потоков энергии. С реактивной душой. С минимальным api. С максимальным использованием нативных конструкций языка.


Построен он на генераторах, из коробки даёт функционал аналогичный react + mobx, не уступает по производительности, при этом весит менее 5kb gzip.


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


Cause & Conse


Причина и следствие. Два базовых потока для организации реактивного состояния данных. Для простоты понимания их можно ассоциировать с привычными computed и observable, они, конечно же, отличаются, но для начала сойдёт.


Начнём со следствия. По сути это одноимённая причина, которой из вне можно задавать значение возвращаемого следствия (маслянистое определение получилось, но чисто технически всё так и есть).


const name = conse('John')// И мы ему такие - What`s up name?whatsUp(name, (v) => console.log(v))// а он нам://> "John"name.set('Barry')//> "Barry"

Пример на CodeSandbox


Ничего особенного, правда? conse создает поток с начальным значением, whatsUp "вешает" наблюдателя. С помощью .set(...) меняем значение наблюдатель реагирует в консоли появляется новая запись.


На самом деле Conse это частный случай потока Cause. Последний создается из генератора, внутри которого выражение yield* это "подключение" стороннего потока к текущему, иными словами обстановку внутри генератора можно рассмотреть так, как будто бы мы находимся внутри изолированной комнаты, в которую есть несколько входов yield* и всего один выход return (конечно же yield ещё, но об этом позже)


const name = conse('John')const user = cause(function* () {    return {        name: yield* name,        //    ^^^^^^ подключаем поток name        //           пускаем его данные в комнату    }})// И мы ему такие - What`s up user? :)whatsUp(user, (v) => console.log(v))// а он нам://> {name: "John"}name.set('Barry')//> {name: "Barry"}

Пример на CodeSandbox


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


И в чем тут соль генераторов?


Действительно, пока что всё это выглядит как-то необоснованно "стероидно". Давайте немного усложним наш пример. Представим, что в данных потока user мы хотим видеть некоторый дополнительный параметр revision, отражающий текущую ревизию.


Сделать это просто мы объявляем переменную revision, значение которой включаем в набор данных потока user и каждый раз в процессе перерасчета увеличиваем на единицу.


const name = conse('John')let revision = 0const user = cause(function* () {    return {        name: yield* name,        revision: revision++,    }})whatsUp(user, (v) => console.log(v))//> {name: "John", revision: 0}name.set('Barry')//> {name: "Barry", revision: 1}

Пример на CodeSandbox


Что-то подсказывает мне, что так не красиво revision выглядит оторваной от контекста и незащищенной от воздействия из вне. Этому есть решение мы можем поместить определение этой переменной в тело генератора, а для отправки нового значения в поток (выхода из комнаты) использовать yield вместо return, что позволит нам не завершать выполнение генератора, а приостанавливать и возобновлять с места последней остановки при следующем обновлении.


const name = conse('John')const user = cause(function* () {    let revision = 0    while (true) {        yield {            name: yield* name,            revision: revision++,        }    }})whatsUp(user, (v) => console.log(v))//> {name: "John", revision: 0}name.set('Barry')//> {name: "Barry", revision: 1}

Пример на CodeSandbox


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


Расширенный пример


Функции cause и conse это шорты для создания потоков. Существуют одноименные базовые классы, доступные для расширения.


import { Cause, Conse, whatsUp } from 'whatsup'type UserData = { name: string }class Name extends Conse<string> {}class User extends Cause<UserData> {    readonly name: Name    constructor(name: string) {        super()        this.name = new Name(name)    }    *whatsUp() {        while (true) {            yield {                name: yield* this.name,            }        }    }}const user = new User('John')whatsUp(user, (v) => console.log(v))//> {name: "John"}user.name.set('Barry')//> {name: "Barry"}

Пример на CodeSandbox


При расширении нам необходимо реализовать метод whatsUp, возвращающий генератор.


Контекст и диспозинг


Единственный агрумент принимаемый методом whatsUp является текущий контекст. В нём есть несколько полезных методов, один из которых update позволяет принудительно инициировать процедуру обновления.


Для избежания ненужных и повторных вычислений все зависимости между потоками отслеживаются динамически. Когда наступает момент, при котором у потока отсутствуют наблюдатели, происходит автоматическое уничтожение генератора. Наступление этого события можно обработать, используя стандартную языковую конструкцию try {} finally {}.


Рассмотрим пример потока-таймера, который с задержкой в 1 секунду, используя setTimeout, генерирует новое значение, а при уничтожении вызывает clearTimeout для очистки таймаута.


const timer = cause(function* (ctx: Context) {    let timeoutId: number    let i = 0    try {        while (true) {            timeoutId = setTimeout(() => ctx.update(), 1000)            // устанавливаем таймер перезапуска с задержкой 1 сек            yield i++            // отправляем в поток текущее значение счетчика            // заодно инкрементим его        }    } finally {        clearTimeout(timeoutId)        // удаляем таймаут        console.log('Timer disposed')    }})const dispose = whatsUp(timer, (v) => console.log(v))//> 0//> 1//> 2dispose()//> 'Timer disposed'

Пример на CodeSandbox


Мутаторы всё из ничего


Простой механизм, позволяющий генерировать новое значение на основе предыдущего. Рассмотрим тот же пример с таймером на основе мутатора.


const increment = mutator((i = -1) => i + 1)const timer = cause(function* (ctx: Context) {    // ...    while (true) {        // ...        // отправляем мутатор в поток        yield increment    }    // ...})

Пример на CodeSandbox


Мутатор устроен очень просто это метод, который принимает предыдущее значение и возвращает новое. Чтобы он заработал нужно всего лишь вернуть его в качестве результата вычислений, вся остальная магия произойдет под капотом. Поскольку при первом запуске предыдущего значения не существует, мутатор получит undefined, параметр i по умолчанию примет значение -1, а результатом вычислений будет 0. В следующий раз ноль мутирует в единицу и т.д. Как вы уже заметили increment позволил нам отказаться от хранения локальной переменной i в теле генератора.


Это ещё не всё. В процессе распространения обновлений по зависимостям, происходит пересчет значений в потоках, при этом новое и старое значение сравниваются с использованием оператора строгого равенства ===. Если значения равны пересчёт останавливается. Это означает, что два массива или объекта с одинаковым набором данных, хоть и эквивалентны, но всё же не равны и будут провоцировать бессмысленные пересчеты. В каких-то случаях это необходимо, в остальных это можно остановить, используя мутатор, как фильтр.


class EqualArr<T> extends Mutator<T[]> {    constructor(readonly next: T[]) {}    mutate(prev?: T[]) {        const { next } = this        if (            prev &&             prev.length === next.length &&             prev.every((item, i) => item === next[i])        ) {            /*            Возвращаем старый массив, если он эквивалентен новому,             планировщик сравнит значения, увидит,             что они равны и остановит бессмысленные пересчеты            */            return prev        }        return next    }}const some = cause(function* () {    while (true) {        yield new EqualArr([            /*...*/        ])    }})

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


Также как cause и conse функция mutator это шорт для краткого определения простого мутатора. Более сложные мутаторы можно описать, расширяя базовый класс Mutator, в котором необходимо реализовать метод mutate.


Смотрите вот так можно создать мутатор dom-элемента. И поверьте элемент будет создан и вставлен в body однократно, всё остальное сведётся к обновлению его свойств.


class Div extends Mutator<HTMLDivElement> {    constructor(readonly text: string) {        super()    }    mutate(node = document.createElement('div')) {        node.textContent = this.text        return node    }}const name = conse('John')const nameElement = cause(function* () {    while (true) {        yield new Div(yield* name)    }})whatsUp(nameElement, (div) => document.body.append(div))/*<body>    <div>John</div></body>*/name.set('Barry')/*<body>    <div>Barry</div></body>*/

Пример на CodeSandbox


Так это ж стейт менеджер на генераторах


Да с одной стороны WhatsUp это стейт менеджер на генераторах, в нём есть аналоги привычных observable, computed, reaction. Есть и action, позволяющий внести несколько изменений и провести обновление за один проход. Пока что ничего необычного, но то что вы увидите дальше, выгодно отличает его от других систем управления состоянием.


Фракталы


Я же говорил, что сохранил идею :) Особенность фрактала заключается в том, что для каждого потребителя он создает персональный генератор и контекст. Он как по лекалу создает новую, параллельную вселенную, в которой своя жизнь, но те же правила. Контексты соединяются друг с другом в отношения parent-child получается дерево контекстов, по которому организуется спуск данных вниз к листьям и всплытие событий вверх к корню. Контекст и система событий, Карл! Пример ниже длинный, но наглядно демонстрирует и то, и другое.


import { Fractal, Conse, Event, Context } from 'whatsup'import { render } from '@whatsup/jsx'class Theme extends Conse<string> {}class ChangeThemeEvent extends Event {    constructor(readonly name: string) {        super()    }}class App extends Fractal<JSX.Element> {    readonly theme = new Theme('light');    readonly settings = new Settings()    *whatsUp(ctx: Context) {        // расшариваем поток this.theme для всех нижележащих фракталов        // т.е. "спускаем его" вниз по контексту        ctx.share(this.theme)        // создаем обработчик события ChangeThemeEvent, которое можно        // инициировать в любом нижележащем фрактале и перехватить тут        ctx.on(ChangeThemeEvent, (e) => this.theme.set(e.name))        while (true) {            yield (<div>{yield* this.settings}</div>)        }    }}class Settings extends Fractal<JSX.Element> {    *whatsUp(ctx: Context) {        // берем поток Theme, расшаренный где-то в верхних фракталах        const theme = ctx.get(Theme)        // инициируем всплытие события, используя ctx.dispath        const change = (name: string) =>             ctx.dispath(new ChangeThemeEvent(name))        while (true) {            yield (                <div>                    <h1>Current</h1>                    <span>{yield* theme}</span>                    <h1>Choose</h1>                    <button onClick={() => change('light')}>light</button>                    <button onClick={() => change('dark')}>dark</button>                </div>            )        }    }}const app = new App()render(app)

Пример на CodeSandbox


Метод ctx.share используется для того, чтобы сделать экземпляр какого-то класса доступным для фракталов стоящих ниже по контексту, а вызов ctx.get с конструктором этого экземпляра позволяет нам найти его.


Метод ctx.on принимает конструктор события и его обработчик, ctx.dispatch в свою очередь принимает инстанс события и инициирует его всплытие вверх по контексту. Для отмены обработки события существует ctx.off, но в большинстве случаев им не приходится пользоваться, поскольку все обработчики уничтожаются автоматически при уничтожении генератора.


Я настолько заморочился, что написал свой jsx-рендер и babel-плагин для трансформации jsx-кода. Уже догадываетесь что под капотом? Да мутаторы. Принцип тот же, что и в примере с мутатором dom-элемента, только тут создается и в дальнейшем мутируется определенный фрагмент html-разметки. Создания и сравнения всего виртуального dom (как в react, например) не происходит. Всё сводится к локальным пересчетам, что даёт хороший прирост в производительности. Иными словами в примере выше, при изменении темы оформления, перерасчеты и обновление dom произойдут только во фрактале Settings (потому что yield* theme поток подключен только там).


Механизм реконсиляции получил свой уникальный алгоритм, побочным эффектом которого оказалась возможность рендерить напрямую в элемент <body>. Вторым аргументом функция render конечно же принимает контейнер, но, как видите в примере выше он не обязательный.


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


Обработка ошибок


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


import { conse, Fractal } from 'whatsup'import { render } from '@whatsup/jsx'class CounterMoreThan10Error extends Error {}class App extends Fractal<JSX.Element> {    *whatsUp() {        const clicker = new Clicker()        const reset = () => clicker.reset()        while (true) {            try {                yield (<div>{yield* clicker}</div>)            } catch (e) {                // ловим ошибку, если "наша" - обрабатываем,                // иначе отправляем дальше в поток и даем возможность                 // перехватить её где-то в вышестоящих фракталах                if (e instanceof CounterMoreThan10Error) {                    yield (                        <div>                            <div>Counter more than 10, need reset</div>                            <button onClick={reset}>Reset</button>                        </div>                    )                } else {                    throw e                }            }        }    }}class Clicker extends Fractal<JSX.Element> {    readonly count = conse(0)    reset() {        this.count.set(0)    }    increment() {        const value = this.count.get() + 1        this.count.set(value)    }    *whatsUp() {        while (true) {            const count = yield* this.count            if (count > 10) {                throw new CounterMoreThan10Error()            }            yield (                <div>                    <div>Count: {count}</div>                    <button onClick={() => this.increment()}>increment</button>                </div>            )        }    }}const app = new App()render(app)

Пример на CodeSandbox


Мне банально непонятен весь этот звездочный код


Согласен, с непривычки это действительно может тяжело восприниматься, поэтому я решил сгруппировать правила в отдельный блок, тем более что их не так много:


  • yield* подключить поток и извлечь из него данные
  • yield отправить данные в поток
  • return отправить данные в поток и пересоздать генератор
  • throw отправить ошибку в поток и пересоздать генератор

Производительность


Естественно этот вопрос нельзя обойти стороной, поэтому я добавил whatsup в проект js-framework-benchmark. Думаю кому-то он известен, но вкратце поясню суть этого проекта заключается в сравнении производительности фреймворков при решении различных задач, как то: создание тысячи строк, их замена, частичное обновление, выбор отдельной строки, обмен двух строк местами, удаление и прочее. По итогам тестирования собирается подробная таблица результатов. Ниже приведена выдержка из этой таблицы, в которой видно положение whatsup на фоне наиболее популярных библиотек и фреймворков таких, как inferno, preact, vue, react и angular



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


Прочие тактико-технические характеристики


Размер


Менее 3 kb gzip. Да это размер самого whatsup. Рендер добавит еще пару кило, что в сумме даст не более 5-ти.


Glitch free


Распространением обновлений занимается специальный планировщик, который производит оценку зависимостей и перерасчет данных в потоках в топологическом порядке. Это позволяет избежать двойных вычислений и "глюков". Не буду дублировать сюда информацию и примеры из википедии, просто оставлю ссылку на статью (Раздел Glitches).


Глубина связей


В комментариях к прошлой статье JustDont справедливо высказался по этому поводу:


Глубина связей данных не может превышать глубину стека вызовов. Да, для современных браузеров это не то, чтоб прямо очень страшно, поскольку счёт идёт минимум на десятки тысяч. Но, например, в хроме некоторой степени лежалости глубина стека вызовов всего лишь в районе 20К. Наивная попытка запилить на этом объемный граф может легко обрушиться в maximum call stack size exceeded.

Я поработал над этим моментом и теперь глубина стека не играет никакой роли. Для сравнения я реализовал один и тот же пример на mobx и whatsup (названия кликабельны). Суть примера заключается в следующем: создаётся "сеть", состоящая из нескольких слоёв. Каждый слой состоит из четырёх ячеек a, b, c, d. Значение каждой ячейки рассчитывается на основе значений ячеек предыдущего слоя по формуле a2 = b1, b2 = a1-c1, c2 = b1+d1, d2 = c1. После создания "сети" происходит вычисление значений ячеек последнего слоя. Затем значения ячеек первого слоя изменяются, что приводит к лавинообразному пересчету во всех ячейках "сети".


Так вот в Chrome 88.0.4324.104 (64-бит) mobx вывозит 1653 слоя, а дальше падает в Maximum call stack size exceeded. В своей практике я однажды столкнулся с этим в одном огромном приложении это был долгий и мучительный дебаг.


Whatsup осилит и 5, и 10 и даже 100 000 слоёв тут уже зависит от размера оперативной памяти компьютера ибо out of memory всё же наступит. Считаю, что такого запаса более чем достаточно. Поиграйтесь в примерах со значением layersCount.


Основу для данного теста я взял из репозитория реактивной библиотеки cellx (Riim спасибо).


О чем я ещё не рассказал


Делегирование


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


Асинхронные задачи


В контексте имеется метод defer, который принимает асинхронный коллбек. Данный коллбек автоматически запускается, возвращает промис, по завершении которого инициируется процедура обновления данных потока.


Роутинг


Вынесен в отдельный пакет @whatsup/route и пока что содержит в себе всего пару методов route и redirect. Для описания шаблона маршрута используются регулярные выражения, не знаю как вам, но в react-router третьей версии мне порой этого очень не хватало. Поддерживаются вложеные роуты, совпадения типа ([0-9]+) и их передача в виде потоков. Там действительно есть прикольные фишки, но рассказывать о них в рамках этой статьи мне кажется уже слишком.


CLI


Не так давно к разработке проекта подключился парень из Бразилии Andr Lins. Наличие интерфейса командной строки для быстрого старта whatsup-приложения целиком и полностью его заслуга.


npm i -g @whatsup/cli# thenwhatsup project

Попробовать


WhatsUp легко испытать где-то на задворках react-приложения. Для этого существует небольшой пакет @whatsup/react, который позволяет сделать это максимально легко и просто.


Примеры


Todos всем известный пример с TodoMVC


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


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


Sierpinski перфоманс тест, который команда реакта показывала презентуя файберы


Напоследок


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


Спасибо за потраченное время, надеюсь я отнял его у вас не зря. Буду рад вашим комментариям, конструктивной критике и помощи в развитии проекта. Да что уж тут даже звездочка на github уже награда.


Кроме того хочу выразить слова благодарности всем тем, кто меня поддерживал, писал в личку, на e-mail, в vk, telegram. Я не ожидал такой реакции после публикации первой статьи, это стало для меня приятной неожиданностью и дополнительным стимулом к развитию проекта. Спасибо!



С уважением, Денис Ч.


"Большая часть моих трудов это муки рождения новой научной дисциплины" Бенуа Мандельброт

Подробнее..

Перевод Вы уверены, что вам нужен API?

16.04.2021 10:20:09 | Автор: admin


От переводчика: При разработке бэкэнда наличие API для фронт-энда стало практически повсеместным стандартом. Однако можем ли мы называть это "настоящим" API? Предлагаем вашему вниманию интересное пятничное чтение, которое, возможно, повлияет на API, которые мы все разрабатываем.


Слабая связанность, сильное сцепление, сокрытие информации. Известные принципы дизайна. Тем не менее, к ним очень часто не относятся серьезно при разработке архитектуры ПО, особенно когда это касается взаимодействия между клиентской и серверной частью (фронтом и бэком на жаргоне разработчиков).


Ценность API в сокрытии информации


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


Почему это важно? Потому что чем объемнее API, тем дороже его поддерживать. Представьте себе крайний случай открытие каждой детали реализации. Каждое изменение в системе может сломать код, который используют ваши клиенты. Именно по этой причине мы стремимся разрабатывать компактные API. Я писал статью (перевод на хабре) по этой теме в контексте описания шаблона Регистрация событий.


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


API как продукт


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


Разделение клиента и сервера


Я готов спорить, что такие API как продукт редкость. Гораздо чаще существование развесистого API признак плохо выбранных границ вследствие нарушения принципа слабая связанность сильное сцепление.


Хотя это исследование также применимо и к коммуникации сервер сервер, я хотел бы сфокусироваться на клиентской части. И я ставлю под вопрос утверждение о том, что нам нужен API для взаимодействия клиента с сервером.


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


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


Тяга к отрисовке на клиентской части


Ещё один двигатель страсть к реализации клиентской части с использованием современных JS фреймворков для разработки пользовательского интерфейса, таких как Angular, React или Vue.js. В противоположность отрисовке на сервере (SSR), эти фреймворки отрисовывают интерфейс на клиентской машине (CSR) в браузере и полагаются на сервисы (REST, GraphQL,...), предоставляемые сервером для получения данных и выполнения каких-то действий.


Самодостаточность и CSR не противоречат друг другу


Хотя я думаю, что SSR сейчас сильно недооценен, я могу это понять, так как требования к клиенту гораздо лучше реализуются современными JS фреймворками.


Но использование такого фреймворка не означает обязательное наличие API! Подумайте о поставке приложения как о самодостаточной, единой системе, которая включает в себя и клиент, и сервер! Уверен, у вас есть разделение сред выполнения, так как серверный код выполняется на сервере, а клиентский в браузере пользователя. И это разделение требует использования REST или GraphQL, чтобы синхронизировать среды выполнения.


Я называю это мостиком, а не API, потому что изменения в серверной части идут рука об руку с изменениями в клиентской. Нет необходимости в тщательном документировании или разбираться с несовместимыми изменениями, потому что у вас есть только один клиент. И этот клиент понятный, так как он расположен ровно в той же самой системе. Вследствие того, что клиент и сервер всегда развертываются как единое целое, исчезают проблемы документирования и совместимости.


Заключение


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


Если вы решите двигаться по пути создания единой системы, то вам решать, что использовать SSR или CSR. И, хотя зачастую понятия CSR и SPA (одностраничное приложение) часто слепливают вместе, вы можете использовать CSR и разрабатывать модульную клиентскую часть.


Ссылки


У меня появилась идея написания этой статьи, когда я слушал подкаст SoftwareArchitekTOUR Episode 82 (German) с Stefan Tilkov и Eberhard Wolff. Спасибо за вдохновение!


Хорошие источники для более глубокого погружения в самодостаточным системам и микро-фронтам:
Self-Contained Systems
Micro Frontend

Подробнее..

Перевод Простая архитектура приложений на фреймворке Angular

31.05.2021 20:20:52 | Автор: admin

Введение в атомарный дизайн

Можете ли вы с первого взгляда определить, что находится внутри общего модуля? И, не открывая другой файл, сказать, в каких компонентах есть зависимости или привязки к другим компонентам? Каким бы ни был ваш ответ, я предлагаю вам заварить чай или кофе и разобраться со мной в основах теории атомарного дизайна, придуманной Брэдом Фростом!

Определение атомарного дизайна в контексте Angular

Структура каталогов при атомарном дизайне в AngularСтруктура каталогов при атомарном дизайне в Angular

Описывая компоненты, разработчики на Angular часто применяют концепцию контейнер презентатор (она же умный компонент глупый компонент). Таким образом можно легко и просто пояснять зависимости и описывать, как компоненты взаимодействуют друг с другом, не заостряя внимание на особенностях реализации пользовательского интерфейса.

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

Атомы и молекулы глупые компоненты

Это простые по сути и удобные в тестировании фрагменты кода, которые могут использоваться многократно. Они получают данные на входе (@Input) и могут что-то выдавать на выходе (@Output). Следовательно, они идеально подойдут для описания библиотек пользовательского интерфейса.

  • Атомы самые крохотные модули, многократно применяемые во всем проекте. Обычно представляют собой одиночный HTML-элемент с базовым стилем.

  • Молекулы отдельная группа атомов.

Организмы и шаблоны простое представление умных компонентов

Шаблоны помогают сократить объемы типового HTML-кода, упрощая умные компоненты, а организмы являются их аналогом для TypeScript.

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

А все дело в том, что их необходимость осознаешь тогда, когда уже поздно. Проектируя сложный умный компонент, мы изначально рассматриваем его как единое целое. Мы создаем полную страницу, постепенно добавляя небольшие фрагменты кода. И тут внезапно этот умный компонент раздувается до пятисот строк TypeScript и тысячи строк HTML-CSS. В получившемся болоте каждый последующий шаг дается сложнее, чем предыдущий. Даже если логика простая, с файлом трудно обращаться из-за его размеров.

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

Шаблоны

Рассматривайте шаблоны как скаффолды и скиныкомпонентов. Обычный код HTML-CSS с элементом ng-content.

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

Давайте рассмотрим простой пример:

Компонент-шаблон, который группирует типовой код оберток наряду со стилем и не обладает логикой.Компонент-шаблон, который группирует типовой код оберток наряду со стилем и не обладает логикой.Страница или умный компонент на основе компонента-шаблона. В нем нет стилей, но он управляет логикой.Страница или умный компонент на основе компонента-шаблона. В нем нет стилей, но он управляет логикой.

Такой подход оправдывает себя не только в отношении небольших фрагментов кода, таких как карточки в Angular Material, но и для макетов на всю ширину экрана. Он полезен в ситуациях, когда не удается обойтись одним компонентом макета с элементом router-outlet или когда нужно разбить код на фрагменты. Как только вы стабилизируете первоначальный код, его рефакторинг под использование шаблонов сводится к простым операциям копировать/вставить и занимает всего несколько минут.

Организмы

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

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

Это умные или глупые компоненты? Однозначного правила в этом отношении нет. Я предпочитаю по возможности собирать все зависимости от сервисов и логику в странице/контроллере. Но имея дело со сложными страницами, иногда разумнее структурировать их как макет и разнести логику на несколько дочерних компонентов-организмов, имеющих собственные зависимости.

Страницы умные компоненты

В общем случае считаются функциональными компонентами в рамках классической структуры проекта типа core/features/shared (базовые модули/ функциональные компоненты/ общие компоненты). Умные компоненты управляют входами/выходами, взаимодействующими с базовыми модулями приложения через сервисы.


Перевод подготовлен в рамках курса "JavaScript Developer. Professional".

Всех желающих приглашаем на открытый урок "Async Patterns в JavaScript". На этом занятии разберем асинхронное программирование в JavaScript, функцию обратного вызова в программировании, функции map, reduce и многое другое. Присоединяйтесь!

Подробнее..

Чем меня не устраивает гексагональная архитектура. Моя имплементация DDD многоуровневая блочная архитектура

15.05.2021 18:08:01 | Автор: admin


* В данной статье примеры будут на TypeScript


Краткое предисловие


Что такое DDD (Domain Driven Design) вопрос обширный, но если в кратце (как Я это понимаю) это про перенос бизнес логики, как она есть, в код, без углубления в технические детали. То есть в идеале, человек, который знает за бизнес процессы, может открыть код и понять, что там происходит (так кстати часто бывает в 1С).


Всё это сопровождается кучей разных рекомендаций по технической реализации вопроса.


Для лучшего понимания статьи советую прочитать материалы, касающиеся DDD.


Гексагональная архитектура это один из подходов реализации DDD.


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


Конкретнее описывать не буду, на хабре полно статей на эту тему, всё давно разжевано и практически переварено.


Вместо этого Я покажу картинку (рис.1):


рис.1


Скажите пожалуйста, что Вам понятно из этой картинки?


Например мне, когда Я первый раз увидел её, было непонятно абсолютно всё.


И, как бы ни было смешно, это первая проблема для меня.


Визуализация должна давать понимание, а не добавлять вопросов.


В ходе изучения всё частично становится на свои места, но вопросы и проблемы остаются.


И тут Я задумался о приложении в общем, развивая идеи вынесенные из DDD в целом и гексагональной архитектуры в частности.


Что мы имеем:


  • Реальная жизнь. Здесь есть бизнес процессы, которые мы должны автоматизировать.
  • Приложение, которое решает проблемы из реальной жизни, которое в свою очередь, не находится в вакууме. У приложения есть:
    1. Пользователи, будь то АПИ, кроны, пользовательские интерфейсы и т.д.
    2. Сам код приложения.
    3. Объекты данных БД, другие АПИ.

Движение идёт сначала сверху вниз, потом обратно, то есть:


  • Субъекты из реальной жизни взаимодействуют с приложением, код приложения взаимодействует с объектами данных, затем получив от них ответ, возвращает его пользователям.

Всё логично.


Теперь углубимся в код приложения.


Как сделать так, чтобы код был понятным, тестируемым, но при этом максимально независимым от внешних объектов данных, таких как БД, АПИ и т.д.?


В ответ на этот вопрос родилась следующая схема (рис.2):


рис.2


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


Ещё одна разница состоит в том, что добавлен уровень бизнес процессов, о котором поговорим ниже.


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


Пробежимся по схеме.


На рисунке (рис.2), слева, мы видим названия сущностей, справа назначение уровней и их зависимости друг от друга.


Сверху вниз:


  1. Порты уровень взаимодействия, который зависит от уровня бизнес процессов. Уровень отвечает за взаимодействие с приложением, то есть хранит контроллеры. Пользоваться приложением можно только через порты.
  2. Ядро приложения уровень бизнес процессов, является центром всех зависимостей. Всё приложение строится исходя из бизнес процессов.
  3. Домены уровень бизнес логики, который зависит от уровня бизнес процессов. Домены образуются и выстраиваются на основании тех бизнес процессов, которые мы хотим автоматизировать. Домены отвечают за конкретную бизнес логику.
  4. Адаптеры уровень агрегации данных, который зависит от уровня бизнес логики. Сверху получает интерфейсы данных, которые должен реализовать. Отвечает за получение и нормализацию данных из объектов данных.
  5. Объекты данных уровень хранения данных, который не входит в приложение, но т.к. приложение не существует в вакууме, мы должны учитывать их.

Несколько правил


По ходу практики родилось и несколько правил, которые позволяют сохранять чистоту, простоту и универсальность кода:


  1. Бизнес процессы должны возвращать однозначный ответ.
    Например создание клиента, при наличии партнерской программы. Можно сделать бизнес процесс, который создает клиента, а если у него есть партнерский код добавляет его ещё и в партнеры, но это не правильно. Из за подобного подхода ваши бизнес процессы становятся непрозрачными и излишне сложными. Вы должны создать 2 бизнес процесса создание клиента и создание партнера.
  2. Домены не должны общаться на прямую между собой. Всё общение между доменами происходит в бизнес процессах. Иначе домены становятся взаимозависимыми.
  3. Все доменные контроллеры не должны содержать бизнес логики, они лишь вызывают доменные методы.
  4. Доменные методы должны быть реализованы как чистые функции, у них не должно быть внешних зависимостей.
  5. У методов все входящие данные уже должны быть провалидированы, все необходимые параметры должны быть обязательными (тут помогут data-transfer-object-ы или просто DTO-шки).
  6. Для unit тестирования уровня нужен нижестоящий уровень. Инъекция (DI) производится только в нижестоящий уровень, например тестируете домены подменяете адаптеры.

Как происходит разработка, согласно этой схеме


  1. Выделяются бизнес процессы, которые мы хотим автоматизировать, описываем уровень бизнес процессов.
  2. Бизнес процессы разбиваются на цепочки действий, которые связаны с конкретными областями (домены).
  3. Решаем как мы храним данные и с какими внешними сервисами взаимодействуем подбираем адаптеры и источники данных, которые наши адаптеры поддерживают. Например в случае с БД мы решаем хранить наши данные в реляционной базе данных, ищем ORM, которая умеет с ними работать и при этом отвечает нашим требованиям, затем под неё выбираем БД, с которой наша ORM умеет работать. В случае с внешними API, часто придется писать свои адаптеры, но опять таки с оглядкой на домены, потому что у адаптера есть 2 главные задачи: получить данные и отдать их наверх в необходимом домену, адаптированном виде.
  4. Решаем как мы взаимодействуем с приложением, то есть продумываем порты.

Небольшой пример


Мы хотим сделать небольшую CRM, хранить данные хотим в реляционной БД, в качестве ORM используем TypeORM, в качестве БД PostgresSQL.


Будет показан не весь код сервера, а лишь основные моменты, которые Вы сможете применить в своём приложении уже сейчас

Для начала реализуем бизнес процесс создания клиента.


Подготовим структуру папок:


рис.3


Для удобства добавим алиасы:


@clients = src/domains/clients@clientsEnities = src/adapters/typeorm/entities/clients@adapters = src/adapters

Из чего состоит бизнес процесс в самом простом виде:


  • на вход мы получаем данные о клиенте
  • нам нужно сохранить его в БД

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


Формируем доменные модели, которые должны реализовать наши адаптеры. В нашем случае это 2 модели: клиент и контактные данные


domains/clients/models/Client.ts

import { Contact } from './Contact';export interface Client {  id: number;  title: string;  contacts?: Contact[];}

domains/clients/models/Contact.ts

import { Client } from './Client';export enum ContactType {  PHONE = 'phone',  EMAIL = 'email',}export interface Contact {  client?: Client;  type: ContactType;  value: string;}

Под них формируем TypeORM enitity


adapters/typeorm/entities/clients/Client.ts

import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';import { Client as ClientModel } from '@clients/models/Client';import { Contact } from './Contact';@Entity({ name: 'clients' })export class Client implements ClientModel {  @PrimaryGeneratedColumn()  id: number;  @Column()  title: string;  @OneToMany((_type) => Contact, (contact) => contact.client)  contacts?: Contact[];}

adapters/typeorm/entities/clients/Contact.ts

import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';import { Contact as ContactModel, ContactType } from '@clients/models/Contact';import { Client } from './Client';@Entity({ name: 'contacts' })export class Contact implements ContactModel {  @PrimaryGeneratedColumn()  id: number;  @Column({ type: 'string' })  type: ContactType;  @Column()  value: string;  @ManyToOne((_type) => Client, (client) => client.contacts, { nullable: false })  client?: Client;}

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


Реализуем доменный метод создания клиента и доменный контроллер.


domains/clients/methods/createClient.ts

import { Repository } from 'typeorm';import { Client as ClientModel } from '@clients/models/Client';import { Client } from '@clientsEnities/Client';export async function  createClient(repo: Repository<Client>, clientData: ClientModel) {  const client = await repo.save(clientData);  return client;}

domains/clients/index.ts

import { Connection } from 'typeorm';import { Client } from '@clientsEnities/Client';import { Client as ClientModel } from '@clients/models/Client';import { createClient } from './methods/createClient';export class Clients {  protected _connection: Connection;  constructor(connection: Connection) {    if (!connection) {      throw new Error('No connection!');    }    this._connection = connection;  }  protected getRepository<T>(Entity: any) {    return this._connection.getRepository<T>(Entity);  }  protected getTreeRepository<T>(Entity: any) {    return this._connection.getTreeRepository<T>(Entity);  }  public async createClient(clientData: ClientModel) {    const repo = this.getRepository<Client>(Client);    const client = await createClient(repo, clientData);    return client;  }}

Т.к. TypeORM немного специфичная библиотека, внутрь мы прокидываем (для DI) не конкретные репозитории, а connection, который будем подменять при тестах.


Осталось создать бизнес процесс.


businessProcesses/createClient.ts

import { Client as ClientModel } from '@clients/models/Client';import { Clients } from '@clients';import { db } from '@adapters/typeorm'; // Я складываю TypeORM соединения в объект dbexport function createClient(clientData: ClientModel) {  const clients = new ClientService(db.connection)  const client = await clients.createClient(clientData)  return  client}

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


Что нам даёт данная архитектура?


  1. Понятную и удобную структуру папок и файлов.
  2. Удобное тестирование. Т.к. всё приложение разбито на слои выберете нужный слой, подменяете нижестоящий слой и тестируете.
  3. Удобное логирование. В примере видно, что логирование можно встроить на каждый этап работы приложения от банального замера скорости выполнения конкретного доменного метода (просто обернуть функцию метода функцией оберткой, которая всё замерит), до полного логирования всего бизнес процесса, включая промежуточные результаты.
  4. Удобную валидацию данных. Каждый уровень может проверять критичные для себя данные. Например тот же бизнес процесс создания клиента по хорошему в начале должен создать DTO для модели клиента, который провалидирует входящие данные, затем он должен вызвать доменный метод, который проверит, существует ли уже такой клиент и только потом создаст клиента. Сразу скажу про права доступа Я считаю что права доступа это адаптер, который Вы должны также прокидывать при создании доменного контроллера и внутри в контроллерах проверять права.
  5. Легкое изменение кода. Допустим Я хочу после создания клиента создавать оповещение, то есть хочу обновить бизнес процесс. Захожу в бизнес процесс, в начале добавляю инциализацию домена notifications и после получения результата создания клиента делаю notifications.notifyClient({ client: client.id, type:SUCCESS_REGISTRATION })

На этом всё, надеюсь было интересно, спасибо за внимание!

Подробнее..

Как мы выбрали архитектуру слоя представления на новом проекте и не прогадали

23.12.2020 08:22:02 | Автор: admin

Про проект

Всем привет! Меня зовут Даниил Климчук. Год назад я пришел в vivid.money третьим Android-разработчиком. Несмотря на это, в проекте практически не было кода, а первые фичи только начинали разрабатываться. Нам нужно было запустить новое банковское приложение в европе, где придется конкурировать с такими компаниями, как Revolut. Уже тогда было понятно, что команда очень быстро значительно вырастет. Конечно, стоило сразу задуматься о том, как будет развиваться архитектура проекта. Через год, когда проект запустится, на это не останется времени, а оправданий вносить значительные изменения просто не будет. Одним из ключевых решений на начальном этапе стал выбор архитектуры слоя представления. В этой статье я поделюсь тем, как мы его принимали.

Про выбор

Возможные подходы для нас явно разделились на две группы: проверенные временем и надежные MVP, MVVM и MVC, а также новые архитектуры, использующие Unidirectional Data Flow: Redux, MVI, Elm (aka MVU) и т.д.. Не хотелось сравнивать каждые в отдельности, а для упрощения определиться в какую сторону смотреть в первую очередь. Поэтому быстро набросали список требований.

Хотелось чтобы:

  • Код был поддерживаемым
    Лучше помнить про то, что с прошествием времени код все еще нужно будет понимать и менять.

  • Новые люди могли быстро влиться.

В данном случае время - деньги, а еще комфортность работы. После прихода в компанию нового всегда хватает и вникать в детали архитектуры может не хватить сил и времени.

  • Уменьшить boilerplate
    Печатать одно и то же на каждом экране утомительно и в добавок может привести к ошибкам.

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

  • Было проще покрыть тестами
    Мы сразу думали о том, что будем покрывать всю логику unit-тестами и хотелось по возможности облегчить себе работу.

Исходя из этого принять решение сразу было достаточно сложно. Пришлось обратиться к проверенному методу и расписать плюсы и минусы обоих подходов:

За старое доброе

  • Нет boilerplate
    Достаточно реализации базовых классов MVP, после этого на каждый экран нужно создавать только Presenter/ViewModel/Controller. В отличие от UDF архитектур, для которых даже каждое событие требует своего класса.

  • Это всем известные архитектуры
    Пожалуй, чтобы найти андроид разработчика, который не работал с MVP нужно еще постараться. А нам важен порог вхождения, ведь необходимо будет собрать большую команду.

  • Проще code review
    При изменении экрана меняется только Presenter и View. В UDF архитектурах логика из Presenter разбивается на несколько классов, каждый из которых приходится просматривать в отдельности.

  • Нет проблемы SingleLiveEvent
    Проблема описана в issue для android architecture components. В MVP в принципе отсутствует, а в MVVM с LiveData можно использовать собственно сам класс SingleLiveEvent. Для UDF архитектур нет устоявшегося подхода с решением этой проблемы, для нее придется придумывать что-то свое.

  • Простота в понимании

Если рассматривать саму архитектуру, то MVP и MVVM определяют только наличие двух классов View и Presenter (или соответственно ViewModel). В UDF архитектурах структура более сложная и у их составляющих более узкая зона ответственности.

За новое хайповое

  • Собственно сам UDF
    В таких архитектурах есть только один фиксированный путь, по которому данные передаются в приложении. В отличие например MVP, где в Presenter со временем может накапливаться огромное количество спагетти-кода, который со временем становится сложно понимать.

  • Single immutable state
    Состояние экрана выделяет в отдельный класс, который называется State. Если нет такого явного ограничения, состояние может описываться множеством флагов иногда частично находиться где-то во View или дублируется в нескольких местах. Такой подход позволяет иметь single source of truth о текущем состоянии экрана. Важным достоинством этого подхода является возможность в каждый момент времени обратиться с State и понять, например, идет ли сейчас загрузка данных.

  • Обработка смены конфигурации и восстановления процесса
    Намного проще, поскольку есть single state. Достаточно просто отрисовать его заново на экране, чтобы полностью восстановить предыдущее состояние. При обработке смерти процесса есть необходимость сохранить только единственный класс. Справедливости ради, например, использование LiveDatа позволит обработать смену конфигурации. Однако это дополнительная зависимость, которую придется тянуть в проект. Также, стандартный механизм обработки смерти процесса для ViewModel на основе SavedStateHandle намного сложнее в реализации и усложняет логику во ViewModel.

  • Separation of Concerns
    Логика слоя представления разделена на несколько классов, каждый из которых выполняет свою функцию. В отличие, например, от MVP в котором все логика находится в Presenter. Получается, что он отвечает за обработку изменения состояния, загрузку данных, изменение модели итд. Явного разделения на зоны ответственности нет и часто она вся находится в одном классе.

  • Thread safety
    Не нужно думать о потокобезопасности, вся синхронизация происходит на уровне реализации архитектуры. Из-за разделения ответственности и неизменяемого состояния различные части кода не должны обращаться к одним и тем же изменяемым данным. Например в MVP в рамках Presenter намного проще выстрелить себе в ногу, случайно поменяв какой-то флаг в состоянии не с главного потока.

  • Проблема bloated presenter
    Со временем Presenter или ViewModel может вырасти до нескольких тысяч строк кода. В этот момент придется думать о том как разделять логику, что может вполне вылиться в решение, менее гибкое, чем изначально заложенная UDF архитектура.

  • Горизонтальное масштабирование

    В некоторых UDF архитектурах есть возможность составлять экран из нескольких частей вместо одного большого Presenter. Например в MviCore есть разделение на Feature, а в ELM - компоненты. Каждая из них написана в одном стиле и вместе они составляют логику экрана. Вдобавок эти части можно переиспользовать, в отличие MVP и MVVM, где придется придумывать свое нестандартное решения этой проблемы.

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

  • Заставляет планировать логику экрана заранее
    Перед написанием экрана изначально приходится подумать о том, каким может быть состояние экрана, какие события будут происходить и как все это связать вместе. В MVP и остальных архитектурах нет четкого подхода к написанию экрана, поэтому планирование остается на совести разработчика.

  • Возможность реализовать Time Travel Debug
    Позволяет записывать последовательность состояний экрана, и потом их воспроизводить. Что позволяет разработчику воспроизвести последовательность действий, приводящих к ошибке.

  • Jetpack Compose
    UDF архитектуры лучше подходят для работы с Jetpack Compose, для которого недавно уже вышла alpha версия. UDF архитектуры имеют единый метод для отрисовки состояния, которое сразу можно преобразовать в иерархию View.

  • Хайп
    Больше шансов, что разработчиков заинтересует вакансия с современной архитектурой, которая даст возможность развиваться или попробовать что-то новое.

Как принимали решение

У UDF архитектур очень много преимуществ, которыми не хотелось жертвовать в угоду простоте. Помимо хайпа и интересных фичей подкупала возможность борьбы с запутанной логикой в Presenter. Не хотелось через несколько лет возвращаться к той же проблеме или оказаться с кучей неподдерживаемого кода. В итоге решили остановиться на UDF.

MVI vs ELM

Многие реализации UDF архитектур сильно похожи, поэтому выделили основное различие: в MVI логика экрана разделена между Reducer и Intent, а в ELM полностью находится в Update.

Например, при нажатии на кнопку загрузки, в MVI Intent знает про то, что нужно получить данные, а reducer отвечает за то, чтобы показать состояние загрузки. В Elm за все это отвечает один класс Update, и только само получение данных происходит в рамках Side Effect.

Почему выбрали ELM

Решили руководствоваться уже существующими недостатками UDF архитектур, в которых были различия. Победил однозначно Elm:

  • Покрытие тестами
    Elm позволяет покрыть тестами всю логику экрана, написав тесты всего на один класс. При этом этот класс не содержит асинхронного кода и писать тесты значительно легче. Более сложные сценарии будут покрываться ui тестами, а работа по написанию unit тестов значительно сократится.

  • Понимание новыми членами команды
    Человеку, который только что пришел работать Elm проще объяснить: "вот здесь логика, а вот здесь асинхронные операции". В отличии от MVI, в котором приходится представлять как все работает в целом.

  • Code review
    Update из Elm можно рассматривать отдельно, поскольку в нем содержится вся логика. При code review кода, написанного на mvi, приходится больше переключаться между Intent и Reducer, потому что логика разделена между ними.

На текущий момент уже есть несколько open-source реализаций Elm архитектуры, например Teapot, Puerh и Elmo, однако мы решили сделать свою.

Как решить проблемы UDF

Остались нерешенными еще два пункта, по ним пришлось искать решения.

Схематично нашу реализацию с итоговым неймингом можно представить вот так:

Boilerplate

Головной болью таких подходов является создание большого числа классов на этапе создания экрана. Например, в нашей реализации это Actor, Reducer, State, Event, Effect, Command и StoreFactory. Простой экран с одним запросом превращается в долгое печатание давно заученного наизусть кода. Для решения этой проблемы был реализован плагин для Android Studio. Весь повторяющийся код можно сгенерировать и добавить новый экран становится не сложнее чем в привычном MVP.

SingleLiveEvent

Мы поддержали решение этой проблемы на уровней нашей реализации Elm. Мы выделили отдельную сущность для сообщений, которые должны быть переданы во View только один раз. Для них выделили отдельную очередь сообщений, на изменения в которой и подписывается View. На схеме эта сущность обозначена как Effect.

Восстановление состояния

Эту проблему можно разделить на две части: восстановление состояния при смене конфигурации и при восстановлении процесса. Для решения первой проблемы хватает хранения Elm компонента внутри Dagger Scope. Новый инстанс фрагмента подключится к компоненту и при инициализации получит последнее состояние. Чуть более сложной получилась обработка смерти процесса. По скольку есть выделенное в отдельный класс состояние, достаточно сохранить его в onSaveInstanceState.

А что дальше

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

Подробнее..

Системный гайд по созданию White Label android-приложений

07.02.2021 14:05:43 | Автор: admin

Как написать код один раз, а продать 20 мобильных приложений? Мы нашли ответ путём проб и факапов и разложили опыт по пунктам: из статьи вы узнаете, как безболезненно реализовать White Label android-проект.

Greetings and salutations! По работе я однажды получил крутую задачу по разработке White Label android-приложения. Изучил достижения коллег в этой области и нашёл только:

  • входные гайды (раз, два, три, etc) о механизмах, но без промышленного дизайна;

  • статьи, в которых освещены узкие аспекты задачи (раз, два, etc).

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

1 Ставим задачу

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

Работает система просто (покупатели показывают приложение на кассе вместо пластиковой карты и получают скидки и бонусы), а для её реализации нужны типовые фичи: регистрация, виртуальная дисконтная карта и прочие.

Бюджет ограничен... фичи типовые... да здравствует конструктор приложений! Или White Label продукт? Пока отложим термины и опишем задачу: генерировать приложения из единой кодовой базы, каждое с дизайном под бренд клиента и только нужными ему фичами.

Задача: создавать приложения для разных клиентов из единой кодовой базы

1.1 Визуализируем решение

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

У каждого бренда своя программа лояльности, например в SEPHORA накапливаются бонусные баллы и процент скидки, а в Пятёрочке только баллы. В приложениях это выглядит так:

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

Как реализовать такой проект без боли? Прочитайте статью и найдёте ответ.

1.2 Детализируем требования

Разложим видение по полочкам: как в ТЗ, но проще.

Функциональные требования

  1. Реализовать общие модули фичей:

    • новости клиент узнаёт об акциях и жизни сети магазинов;

    • лояльность получает дисконтную карту, узнаёт баланс, пробивает на кассе;

    • ...

  2. Задавать отдельно для каждого приложения:

    • наборы фичей, чтобы выбирать сами модули и настраивать их параметры;

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

Нефункциональные

  • у приложений должен быть общий код;

  • настройка нового приложения меньше четырёх часов разработчика;

  • архитектура должна упрощать расширение модулей и поддержку от 10 до 100 приложений.

1.3 Что пилим то? Конструктор? White Label?

Требования ясны разберёмся с терминами. На старте проекта я думал, что мы делаем конструктор приложений. Искал статьи с опытом создания, а находил рекламу готовых продуктов. Потратил пару часов на гугл и с радостью сэкономлю время вам:

  1. Что даёт конструктор/платформа:

    • универсальный сервис сборки приложений из готовых компонентов;

    • например, AppGyver dragndrop вёрстка, программирование на низкоуровневых фичах (открыть экран, сделать фото);

    • творим что угодно от приложений по покупке золота до приёмки грузов.

  2. Что даёт White Label:

    • конструктор для конкретного типа приложений, например для такси;

    • ребрендинг под клиента и настройка высокоуровневых фич (новости, профиль)

Наш фокус на системах лояльности. Значит, делаем White Label. Гуглим white label android development и находим то, что нужно.

2 Проектируем и воплощаем

Строим системную схему White Label приложения

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

и получим четыре жирные проблемы:

  1. Как шарить кодовую базу между приложениями?

  2. Как сделать ребрендинг?

  3. Как задавать конфиги?

  4. Как отключать ненужные модули и настраивать необходимые?

В очередь, проблемы, в очередь!

2.1 Шарим код

Задача одна кодовая база, до 100 приложений. Решение Gradle Product Flavors.

Если вы ещё не знакомы с Gradle Product Flavors, советую почитать документацию или общие статьи. А можно и сразу в контексте White Label: кратко или в формате инструкции

В двух словах, создаём варианты базового приложения. Каждый получает изолированные папки с кодом и ресурсами, сохраняя доступ к общим из main.

Главное преимущество. Относительная простота переиспользования кода и ресурсов, удобство сборки.

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

Альтернативы, на мой взгляд, рассматривать нет смысла: решение надёжное, из коробки.

Пример flavors. Допустим, на старте делаем два приложения:

  1. Лояка абстрактная компания;

  2. Ювелирия сеть ювелирных магазинов.

Назовём flavors соответственно loyaka и jewelry. Сразу реализуем best practice конфиг каждого flavor вынесем в отдельный файлик. Зачем? Станет ясно чуть позже.

Пока создадим:

  1. папку project_flavors;

  2. в ней gradle-скрипты flavor_loyaka.gradle, flavor_jewelry.gradle и flavors_common.gradle;

  3. задействуем скрипты в build.gradle уровня app.

Здесь и далее привожу сокращённые примеры из тестового проекта к статье.

flavor_loyaka.gradle

apply from: "$rootDir/project_flavors/flavors_common.gradle"  android {    productFlavors {        loyaka {            dimension APP_DIMENSION            resValue "string", APP_NAME_VAR, 'Лояка'            applicationId BASE_PACKAGE + 'loyaka'        }    }}

flavor_jewelry.gradle

apply from: "$rootDir/project_flavors/flavors_common.gradle"  android {    productFlavors {        jewerly {            dimension APP_DIMENSION            resValue "string", APP_NAME_VAR, 'Ювелирия'            applicationId BASE_PACKAGE + 'jewelry'        }    }}

flavors_common.gradle

android {    ext.DIMENSION_APP = "app"    ext.APP_NAME_VAR = "app_name"    ext.BASE_PACKAGE = "com.livetyping."}

Наконец, задействуем flavors в build.gradle уровня app:

...apply from: "$rootDir/project_flavors/flavor_loyaka.gradle"apply from: "$rootDir/project_flavors/flavor_jewelry.gradle"apply from: "$rootDir/project_flavors/flavors_common.gradle"android {    ...    flavorDimensions APP_DIMENSION}...

2.2 Перекрашиваем

2.2.1 Концепт

У каждого приложения свой бренд, который складывается из:

  • цветовой схемы;

  • шрифтов, картинок, строк;

  • зашитого контента (соглашений, ссылок в соц. сети).

Благодаря flavors тоже решим задачу просто. Загрузим в голову 3 факта:

  1. общие код и ресурсы проекта лежат в папке main;

  2. для gradle main это как дефолтный flavor;

  3. у каждого flavor свои исходники. Например, общие ресурсы лежат в main/res, а специфичные для флэйвора loyaka в loyaka/res;

Что произойдёт, если в main/res и loyaka/res будут картинки с одинаковым именем animal.webp? Возникнет конфликт, и чтобы решить его, Gradle переопределит базовые ресурсы кастомными. Если непонятно, поможет диаграмма:

Слева ресурсы по flavor; справа итоговый APK.Слева ресурсы по flavor; справа итоговый APK.

Задача решена! Уберём дефолтные ресурсы в main, а в конкретных flavor будем переопределять по необходимости.

2.2.2 Best practices

Крайне важно заранее договориться с дизайнерами:

  • ресурсы в приложениях называем одинаково вставляем в проект прямиком из дизайна;

  • тему задаём чётким набором цветов для перекрашивания копируем colors.xml в новый flavor и просто меняем значения.

И, конечно, соблюдаем договорённости, ведь впереди ждут испытания. Например, мы сразу решили, что задаём строгий набор цветов. Однако в очередном приложении цвета не сошлись часть элементов цвета primary в новом дизайне стали accent. Сразу обсудили и изменили дизайн, а ведь могли бы и вставить костыль.

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

Создать и поддерживать чёткую схему для всех приложений на порядок дешевле, чем расширять код, состоящий из уникальных косяков.

2.2.3 Пример схемы цветов

Задаём цвета бренда в файле project_styleguide.xml:

<?xml version="1.0" encoding="utf-8"?><resources>    <color name="active">#68b881</color>    <color name="background">#36363f</color>    <color name="disabled">#daede0</color>    <color name="field_dark">#f5f5f5</color>    ...</resources
<?xml version="1.0" encoding="utf-8"?><resources>    <color name="active">#a160d5</color>    <color name="background">#f6ebff</color>    <color name="disabled">#e2c8f6</color>    <color name="field_dark">#f5f5f5</color>    ...</resources>

2.3 Задаём конфиг

2.3.1 Концепт

Фичи настраиваем на двух уровнях:

  1. отключаем ненужные модули;

  2. меняем параметры внутри самих модулей.

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

Упрощённый пример дока в формат модуль-фича-параметры:

  1. Подключаемые модули:

    • лояльность;

    • новости;

  2. Аутентификация:

    • логин пользователя: телефон или email;

    • маска логина.

  3. Карта лояльности:

    • тип штрих-кода: EAN-8, EAN-13, CODE-128.

2.3.2 Пути решения

Как сделать качественный конфиг? Само качество определим так:

  1. удобство работы простота чтения, простота заполнения (в идеале, хотим DSL);

  2. Скорость обработки важно, чтобы чтение конфига не тормозило приложение.

Выделим основные пути:

  1. Gradle buildConfigField

    • задаём переменные в gradle скрипте;

    • во время компиляции генерится java класс BuildConfig, переменные доступны как его поля.

  2. JSON

    • json объект в файле;

    • зашит локально, либо получаем с сервера.

Кратко оценим пути по критериям.

2.3.3 Путь 1. Gradle buildConfigField

Плюсы:

  • удобство создания делаем DSL на минималках: выносим типы и возможные значения параметров в переменные; выявляем синтаксические ошибки на компиляции;

  • простота большинству уже знаком;

  • скорость обращаемся к классу BuildConfig в памяти.

Главный минус отвратительная читаемость: переменная состоит из четырёх блоков, подсветки синтаксиса нет.

Пример переменной на условном DSL:

buildConfigField MAIN_SCREEN_TYPE, MAIN_SCREEN_VAR, MS_SHOPS

2.3.4 Путь 2. JSON

Плюсы:

  • удобство чтения особенно в формате HOCON;

  • удобство создания делаем DSL через JSON Schema, проверяем на ошибки по мере написания;

  • переиспользование шарим между iOS и Android.

Минусы:

  • скорость придётся перед запуском считать из файла или получать с сервера;

  • время на освоение по сравнению с первым вариантом JSON Schema наверняка менее популярна.

2.3.5 Так что же лучше?

Когда делали проект, даже не изучали альтернативы. Сразу сделали через Gradle. На мой взгляд, JSON + Schema его побеждает. Удобство чтения приоритет, при этом удобство создания остаётся на том же уровне, если не лучше. Дополнительная секунда для загрузки файла на общем фоне незначительна.

Сделали конфиг через Gradle, не изучая альтернатив. Но оказалось, что JSON Schema удобнее для чтения и это её главное преимущество.

2.3.6 Best practices для buildConfigField

Если выбрали buildConfigField, то в сыром виде с ним будут проблемы:

  1. чтобы использоватьEnum, придётся указать полный путь к пакету как в типе, так и в значениях;

  2. при изменение имени или типа переменной придётся делать Find & Replace по всем конфигам.

Решение: DSL на минималках. Заводим переменные для названий параметров, а также кастомных типов и вариантов значений. Создаём отдельный gradle-скрипт на каждый модуль. Параметры описываем в формате экран-параметр-переменные. Скрипты кладём в папку business_rules.

Пример: модуль лояльности loyalty_business_rules.gradle:

/*_______________ENTER USER ID________________*//*________User ID________*//*__Variable__*/ext.USER_ID_VAR = "USER_ID"ext.USER_ID_TYPE = "com.example.whitelabelexample.domain.models.UserIdType"/*__Values__*/ext.UI_PHONE = USER_ID_TYPE + ".PHONE"ext.UI_EMAIL = USER_ID_TYPE + ".EMAIL"/*_______________NO CARD________________*//*________Obtain card methods________*//*__Variable__*/ext.OBTAIN_METHODS_VAR = "OBTAIN_CARD_METHODS"ext.OBTAIN_METHODS_ENUM = "com.example.whitelabelexample.domain.models.ObtainCardMethod"ext.OBTAIN_METHODS_TYPE = "java.util.List<" + OBTAIN_METHODS_ENUM + ">"/*__Optional values__*/ext.OM_GENERATE = OBTAIN_METHODS_ENUM + ".GENERATE_VIRTUAL"ext.OM_BIND = OBTAIN_METHODS_ENUM + " .BIND_PHYSICAL"...

UI_PHONE что за UI_? Это сокращение переменной UserId: добавляем префиксы, чтобы избежать коллизий.

Дальше настраиваем приложения в скриптах flavor, которые на первом шаге заботливо вытащили по файлам.

Пример: flavor_loyaka.gradle:

...loyaka {    ...    /* MAIN SCREEN */    buildConfigField MAIN_SCREEN_TYPE, MAIN_SCREEN_VAR, MS_CARD    /* MODULES */    buildConfigField APP_MODULES_TYPE, APP_MODULES_VAR, list(AM_LOYALTY, AM_SHOWCASE)    /* REGISTRATION */    buildConfigField USER_ID_TYPE, USER_ID_VAR, UI_EMAIL    ...}

flavor_jewelry.gradle:

...jewelry {    ...    /* MAIN SCREEN */    buildConfigField MAIN_SCREEN_TYPE, MAIN_SCREEN_VAR, MS_SHOPS    /* MODULES */    buildConfigField APP_MODULES_TYPE, APP_MODULES_VAR, list(AM_LOYALTY, AM_SHOPS)    /* REGISTRATION */    buildConfigField USER_ID_TYPE, USER_ID_VAR, UI_PHONE    ...}

2.3.7 Получаем доступ к конфигу

Спроектируем решение в контексте Clean Architecture.

Классы конфигов приравниваю к источникам в слое data, ибо они только предоставляют данные. Тогда ui получает параметры посредством domain.

Как сгруппировать параметры по классам? Мы на проекте за группу взяли экран, а зря. С одной стороны, компактные классы, а с другой возникли коллизии, например на экранах ввода номера телефона и подтверждения кода нужна маска номера. Пришлось реализовать считывание из конфига 2 раза.

С BuildConfig это легко, но с JSON будет грязно. Считаю, что оптимально группировать по процессу (флоу). Под процессом здесь понимаю целевой use case и вторичные по отношению к нему. Обычно это группа экранов, например в модуле лояльности два целевых процесса:

  1. аутентификация чтобы авторизоваться, придётся ввести логин, а затем код подтверждения;

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

Пример реализации конфига для второго процесса: фрагмент BuildCardConfig.kt:

class BuildCardConfig : CardConfig {    override fun numberMask(): String = BuildConfig.CARD_NUMBER_MASK    override fun barcodeType(): BarcodeType = BuildConfig.BARCODE_TYPE    override fun obtainmentMethods(): List<ObtainCardMethod> = BuildConfig.OBTAIN_CARD_METHODS    ...}

В итоге получим архитектуру работы с конфигом (диаграмма классов UML; в ui MVVM):

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

2.3.8 Валидируем конфиг

Зачем нужна прослойка в domain? Она же будет пустая! Необязательно. В идеале, хотим защиту от дурака проверку параметров фичей на непротиворечивость. Допустим, дано 2 параметра:

  1. включённые модули;

  2. главный экран.

Если модуль новости и акции выключён, то логично, что главным экраном новости быть не может. Но на уровне Gradle или JSON Schema подобное ограничение сделать нетривиально таким правилам и место в domain.

Например, реализуем описанное условие в GetMainTabUseCase.kt:

class GetMainTabUseCase(    private val mainConfig: MainConfig) {    operator fun invoke(): NavigationTab {        val mainTab = mainConfig.mainTab()        val mainModule = tabsByModules.entries.find { it.value == mainTab }!!.key        val isModuleEnabled = BuildConfig.APP_MODULES.contains(mainModule)        if (isModuleEnabled.not()) {            throw IllegalStateException("Can't use a tab ($mainTab) as main, it's module is disabled   fix config!")        }        return mainTab    }}

Возникает проблема: если создавать UseCase на каждый параметр, то будет много пустых классов. Ведь правила есть не везде.

Альтернатива создавать UseCase только по надобности, но тогда возникает неоднородность: в ui используются одновременно и Config и UseCase. Рискуем использовать параметры, которые требуют валидации, в её обход, и следом за этим растёт вероятность багов.

Впрочем, если правил у вас субъективно мало и коллизии очевидные, такая валидация не нужна. Мы обошлись без неё и багов из-за этого пока не ловили.

2.4 Настраиваем фичи

2.4.1 Выбираем модули

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

Модуль здесь крупный связный и независимый кусок функциональности.

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

Рассмотрим главные точки связи модуля с приложением:

  1. Переходы изui боттом навигация, рандомная кнопка, etc;

  2. Реакция на события кастомные (выбран город), платформы (найдена сеть), etc.

По опыту, обычно хватит убрать пункт модуля из меню, например, для лояльности это таб из боттом навигации. Я реализовал пример в MainViewModel и MainActivity тестового проекта.

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

На события в Лояке реагирует только модуль пушей. Когда юзер выбирает свой город подписываемся на соответствующий новостной канал. Опять же обрабатываем каждый кейс.

Всплывает неочевидный минус buildConfigField придётся указывать параметры даже для выключенных модулей, хотя бы как null, иначе проект не соберётся.

2.4.2 Настраиваем экраны и бизнес-правила

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

На уровне UseCase берём параметр из нужного класса Config.

GetCardUseCase.kt:

class GetCardUseCase(    private val netRep: CardNetRepository,    private val storageRep: CardStorageRepository,    private val config: CardConfig) {    operator fun invoke(): Card? {        return if (config.isCacheCard()) {            try {                val card = netRep.getCard()                storageRep.save(card)                card            } catch (exception: Exception) {                return storageRep.get()            }        } else {            netRep.getCard()        }    }}

В ui же обращаемся к UseCase на уровне ViewModel или Presenter.

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

Реализация: NoCardViewModel.kt:

class NoCardViewModel(    private val getObtainMethodsUseCase: GetObtainMethodsUseCase,    ...){    private val cardObtainMethods by lazy { getObtainMethodsUseCase() }    val isShowGetVirtualButton by lazy {        cardObtainMethods.contains(ObtainCardMethod.GENERATE_VIRTUAL)    }    val isShowBindPlasticButton by lazy {        cardObtainMethods.contains(ObtainCardMethod.BIND_PHYSICAL)    }    ...}

fragment_nocard.xml:

...<com.google.android.material.button.MaterialButton    android:id="@+id/no_card_bind_plastic_button"    ...    app:isVisible="@{viewmodel.isShowBindPlasticButton}" /><com.google.android.material.button.MaterialButton    android:id="@+id/no_card_get_virtual_button"    ...    app:isVisible="@{viewmodel.isShowGetVirtualButton}" />...

2.4.3 Ещё один трюк

Иногда вариативную вёрстку целесообразнее сделать без конфига.

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

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

3 Подведём итог

Мы успешно спроектировали архитектуру White Label android-проекта, которая соответствует поставленным требованиям, а именно позволяет:

развивать общую кодовую базу расширять модули фичей и собирать из одного кода разные приложения, от 10 до 100;

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

Горькими уроками поделились, best practices передали. Надеюсь, наш опыт создал цельное представление о создании White Label android-приложений и комфортную отправную точку для вашего проекта.

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

Если возникли вопросы пишите в комменты, буду рад ответить!

4 Куда развить решение?

  • Мы продаём целую систему, а что если продавать модули в другие приложения? Тот же White Label, но на системный уровень ниже. Мы такую задачу решали, если интересно напишите в комментах, расскажем.

  • Когда количество приложений растёт, хочется CI и CD. В этом репозитории есть подробный гайд по настройке Azure Devops.

  • Если не нужна детальная настройка фичей, а писать flavors руками надоело сделайте автогенерацию flavors по json конфигу.

  • Бизнес бьёт ключом, клиентов больше сотни? Пора автоматизировать создание приложений.

Если знаете кейсы, на которые нет ссылок в нашей статье, то обязательно скиньте их в комменты вместе мы точно соберём крутую библиографию!

P.S. Shout-out дорогим коллегам за работу над проектом и помощь в написании статьи - без вас это было бы невозможно :)

Подробнее..

Пишем под android с Elmslie

20.04.2021 08:05:05 | Автор: admin

Вступление

Это третья часть серии статей об архитектуре android приложения vivid.money. В ней мы расскажем об Elmslie - библиотеке для написания кода под android с использованияем ELM архитектуры. Мы назвали ее в честь Джорджа Эльмсли, шотландского архитектора. С сегодняшнего дня она доступна в open source. Это реализация TEA/ELM архитектуры на kotlin поддержкой android. В первой статье мы рассказали о том почему выбрали ELM. Перед прочтением этой статьи лучше ознакомиться как минимум со второй частью, в которой мы более подробно рассказывали том собственно такое ELM.

Оглавление

Что будем писать

В предыдущей статье мы рассказывали о том что такое ELM архитектура. В ней приводился пример, его и будем реализовывать. В нем происходит загрузка числового значения и есть возможность его обновлять. Исходный код для этого примера доступен на GitHub.

Модель

Написание экрана проще начинать с проектирования моделей. Для каждого экрана нужны State, Effect, Command и Event. Рассмотрим каждый из них по очереди:

State

State описывает полное состояние экрана. В каждый момент времени по нему можно полностью восстановить то что сейчас показывается пользователю. Правильнее делать исключения, если это усложняет логику. Не нужно сохранять, показывается ли сейчас диалог на экране или что сейчас находится в каждом EditText.

На нашем экране будет отображаться либо числовое значение, либо состояние загрузки. Это можно задать двумя полями в классе: val isLoading: Boolean и val value: Int?. Для удобства изменения, State лучше реализовывать как data class. В итоге получается так:

data class State(  val isLoading: Boolean = false,  val value: Int? = null)

Effect

Каждый Effect описывает side-effect в работе экрана. То есть это события, связанные с UI, происходящие ровно один раз, причем только когда экран виден пользователю. Например, это могут быть навигация, показ диалога или отображение ошибки.

В нашем примере единственной командой UI будет показ Snackbar при ошибке загрузки value. Для этого заведем Effect ShowError. Для удобства Effect можно создавать как sealed class, чтобы не забыть обработать новые добавленные эффекты:

sealed class Effect {  object ShowError : Effect()}

Command

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

У нас будет одна операция - загрузить данные. Эту Command назовем LoadValue. Команды так же удобнее задавать как sealed class:

sealed class Command {  object LoadValue : Command()} 

Event

Все события, которые влияют на состояние и действия на экране: Ui: ЖЦ экрана, взаимодействие с пользователем, все что приходит из View слоя Internal: Результаты операций с бизнес логикой

Теперь перейдем к событиям. В нашем проекте мы разделяем события на две категории:

  • Event.UI: все события, которые происходят во View слое

  • Event.Internal: результаты выполнения команд в Actor.

В этом примере будет два UI события: Init - открытие экрана и ReloadClick - нажатие на кнопку обновления значение. Internal события тоже два: ValueLoadingSuccess - успешный результат Command LoadValue и ValueLoadingError, которое будет отправляться при ошибке загрузки значения.

Если использовать разделение на UI и Internal, то Event удобнее задавать как иерархию sealed class:

sealed class Event {  sealed class Ui : Event() {    object Init : Ui()    object ReloadClick : Ui()  }     sealed class Internal : Event() {    data class ValueLoadingSuccess(val value: Int) : Internal()    object ValueLoadingError : Internal()  }}

Реализуем Store

Закончив с моделями, перейдем собственно к написанию кода. Сам Store реализовывать не нужно, он предоставляется библиотекой классом ElmStore.

Repository

Для нашего примера напишем симуляцию работы с моделью, которая будет возвращать либо случайный Int, либо ошибку:

object ValueRepository {private val random = Random()fun getValue() = Single.timer(2, TimeUnit.SECONDS)    .map { random.nextInt() }    .doOnSuccess { if (it % 3 == 0) error(&quot;Simulate unexpected error&quot;) }}

Actor

Actor - место, в котором выполняются долгосрочные операции на экране - запросы в сеть, подписка на обновление данных, итд. В большинстве случаев, как и в нашем примере просто маппит результаты запросов в Event.

Для его создания нужно реализовать интерфейс Actor, который предоставляется библиотекой. Actor получает на вход Command, а результатом его работы должен быть Observable<Event>, с событиями, которые сразу будут отправлены в Reducer. Для удобства в библиотеке есть функции mapEvents, mapSuccessEvent, mapErrorEvent и ignoreEvents, которые позволяют преобразовать данные в Event.

В нашем случае Actor будет выполнять только одну команду. При выполнении команды загрузки мы будем обращаться к репозиторию. В случае получения успешного значения будет оправляться событие ValueLoaded, а при ошибке ErrorLoadingValue. B итоге получается такая реализация:

class Actor : Actor<Command, Event> {override fun execute(command: Command): Observable&lt;Event&gt; = when (command) {    is Command.LoadNewValue -&gt; ValueRepository.getValue()        .mapEvents(Internal::ValueLoaded, Internal.ErrorLoadingValue)}}

Reducer

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

В этом классе нужно реализовать функцию reduce для обработки событий. Помимо вашей логики в Reducer можно использовать 3 функции:

  • state - позволяет изменить состояние экрана

  • effects - отправляет эффект во View

  • commands - запускает команду в Actor

class Reducer : DslReducer<Event, State, Effect, Command>() {override fun Result.reducer(event: Event) = when (event) {    is Internal.ValueLoaded -&gt; {        state { copy(isLoading = false, value = event.value) }    }    is Internal.ErrorLoadingValue -&gt; {        state { copy(isLoading = false) }        effects { +Effect.ShowError }    }    is Ui.Init -&gt; {        state { copy(isLoading = true) }        commands { +Command.LoadNewValue }    }    is Ui.ClickReload -&gt; {        state { copy(isLoading = true, value = null) }        commands { +Command.LoadNewValue }    }}}

Собираем Store

После того как написаны все компоненты нужно создать сам Store:

fun storeFactory() = ElmStore(    initialState = State(),    reducer = MyReducer(),    actor = MyActor()).start()

Экран

Для написания android приложений в elmslie есть отдельный модуль elmslie-android, в котором предоставляются классы ElmFragment и ElmAсtivity. Они упрощают использование библиотеки и имеют схожий вид. В них нужно реализовать несколько методов:

  • val initEvent: Event - событие инициализации экрана

  • fun createStore(): Store - создает Store

  • fun render(state: State) - отрисовывает State на экране

  • fun handleEffect(effect: Effect) - обрабатывает side Effect

В нашем примере получается такая реализация:

class MainActivity : ElmActivity<Event, Effect, State>() {override val initEvent: Event = Event.Ui.Initoverride fun onCreate(savedInstanceState: Bundle?) {    super.onCreate(savedInstanceState)    setContentView(R.layout.activity_main)    findViewById&lt;Button&gt;(R.id.reload).setOnClickListener {        store.accept(Event.Ui.ClickReload)     }}override fun createStore() = storeFactory()override fun render(state: State) {    findViewById&lt;TextView&gt;(R.id.currentValue).text = when {        state.isLoading -&gt; &quot;Loading...&quot;        state.value == null -&gt; &quot;Value = Unknown&quot;        else -&gt; &quot;Value = ${state.value}&quot;    }}override fun handleEffect(effect: Effect) = when (effect) {    Effect.ShowError -&gt; Snackbar        .make(findViewById(R.id.content), &quot;Error!&quot;, Snackbar.LENGTH_SHORT)        .show()}}

Заключение

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

Подробнее..

Перевод Эффективная конструкция агрегатов. Моделирование одиночного агрегата

21.02.2021 12:18:25 | Автор: admin

Эта статья является конспектом материала Effective Aggregate Design Part I: Modeling a Single Aggregate.

Объединение сущностей (entities) и объектов значений (value objects) в агрегат с тщательно продуманными границами согласованности может показаться простым, но из всех тактических DDD шаблонов, агрегат является одним из самых сложных.

Для начала будет полезно рассмотреть некоторые общие вопросы. Является ли агрегат просто способом объединения тесно связанных объектов с общим корнем (Aggregate Root)? Если да, то есть ли какое-то ограничение на количество объектов, которые могут находиться в графе? Поскольку один агрегат может ссылаться на другой, можно ли перемещаться по агрегатам с помощью этих связей и менять данные объектов, входящих в определенный агрегат? И чем является инвариант и граница согласованности? Ответ на последний вопрос в значительной степени влияет на остальные ответы.

Есть множество способов смоделировать агрегат неправильно. Мы можем спроектировать слишком большой агрегат. С другой стороны, мы можем разделить все агрегаты так, что в результате нарушатся истинные инварианты. Как мы увидим, крайне важно избегать подобных крайностей и вместо этого обращать внимание на бизнес-правила.

Разработка приложения ProjectOvation

Давайте рассмотрим агрегаты на примере. Наша фиктивная компания разрабатывает приложение для поддержки проектов, основанных на методологии Scrum. Приложение следует традиционной модели управления проектами по методологии Scrum, то есть имеются продукт (product), владелец продукта (product owner), команды (team), элементы бэклога (backlog items), запланированные релизы (planned releases), спринты (sprints). Терминология Scrum формирует стартовую точку единого языка (ubiquitous language). Каждая организация, которая покупает подписку, регистрируется как арендатор (tenant), это еще один термин для нашего единого языка.

Компания собрала группу талантливых разработчиков. Однако, их опыт с DDD несколько ограничен. Это означает, что команда будет допускать ошибки, связанные с DDD по ходу разработки. Они будут расти, и мы вместе с ними. Их трудности помогут нам распознать и устранить подобные неблагоприятные ситуации, которые мы создали в нашем собственном программном обеспечении.

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

  • Продукты имеют элементы бэклога, релизы и спринты.

  • Можно добавлять новые элементы бэклога.

  • Можно добавлять новые релизы.

  • Можно добавлять новые спринты.

  • Запланированный элемент бэклога можно привязать к релизу.

  • Запланированный элемент бэклога можно привязать к спринту.

На основе этих утверждений команда спроектировала первый вариант модели. Давайте посмотрим, что у них вышло.

Первая попытка: большой агрегат

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

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

  • Если спринт имеет элементы бэклога, то мы не должны позволить удалить его из системы.

  • Если релиз имеет запланированные элементы бэклога, то мы не должны позволить удалить его из системы.

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

В результате Product был смоделирован как очень большой агрегат. Корневой объект, Product, содержит все BacklogItem, все Release, все Sprint экземпляры, связанные с ним. Такой интерфейс защищал все детали от случайного удаления клиента. Эта конструкция показана в следующем коде и в виде UML-диаграммы ниже.

public class Product extends ConcurrencySafeEntity {    private Set<BacklogItem> backlogItems;    private String description;    private String name;    private ProductId productId;    private Set<Release> releases;    private Set<Sprint> sprints;    private TenantId tenantId;    ...}
Рис. 1. Product смоделирован как очень большой агрегат.Рис. 1. Product смоделирован как очень большой агрегат.

Большой агрегат выглядел привлекательно, но он не был по-настоящему практичен. Как только приложение стало работать в предполагаемой многопользовательской среде, начинали происходить регулярные сбои транзакций. Наши экземпляры агрегата используют оптимистическую блокировку для защиты объектов от одновременной модификации несколькими клиентами, что позволяет избежать использования блокировок БД. Объекты содержат номер версии, который увеличивается во время внесения изменений и проверяется перед тем, как эти изменения будут сохранены в БД. Если версия сохраняемого объекта больше версии (копии) клиента, то версия клиента считается устаревшей и обновления отклоняются.

Рассмотрим общий многопользовательский сценарий:

  • Два пользователя, Билл и Джо, смотрят одинаковый Product c версией 1 и начинают работать с ним.

  • Билл планирует новый BacklogItem и сохраняет. Версия становится 2.

  • Джо планирует новый Release и пытается сохранить, но он получает ошибку, так как версия его копии Product устарела и равнялась 1.

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

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

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

Вторая попытка: несколько агрегатов

Теперь рассмотрим альтернативную модель, которая показана на рисунке 2. У нас есть четыре агрегата. Каждая зависимость использует ProductId, который является идентификатором Product-а.

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

Разбиение большого агрегата на четыре изменит контракт метода для Product. С большим агрегатом сигнатуры методов выглядели следующим образом:

public class Product ... {    ...      public void planBacklogItem(        String aSummary, String aCategory,        BacklogItemType aType, StoryPoints aStoryPoints) {      ...      }    ...      public void scheduleRelease(        String aName, String aDescription,        Date aBegins, Date anEnds) {      ...      }      public void scheduleSprint(        String aName, String aGoals,        Date aBegins, Date anEnds) {        ...      }      ...}

Все эти методы являются командами. То есть они модифицируют состояние Product, добавляя новый элемент в коллекцию, поэтому их возвращаемый тип void. Но с отдельными агрегатами мы имеем:

public class Product ... {    ...      public BacklogItem planBacklogItem(        String aSummary, String aCategory,        BacklogItemType aType, StoryPoints aStoryPoints) {      ...      }        public Release scheduleRelease(        String aName, String aDescription,        Date aBegins, Date anEnds) {        ...      }      public Sprint scheduleSprint(        String aName, String aGoals,        Date aBegins, Date anEnds) {        ...      }      ...}

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

public class ProductBacklogItemService ... {     ...     @Transactional     public void planProductBacklogItem(           String aTenantId, String aProductId,           String aSummary, String aCategory,           String aBacklogItemType, String aStoryPoints) {           Product product =                   productRepository.productOfId(                                 new TenantId(aTenantId),                                new ProductId(aProductId));           BacklogItem plannedBacklogItem =                  product.planBacklogItem(                            aSummary,                            aCategory,                            BacklogItemType.valueOf(aBacklogItemType),                            StoryPoints.valueOf(aStoryPoints));                    backlogItemRepository.add(plannedBacklogItem);      }      ...}

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

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

Моделируйте истинные инварианты в контексте согласованности

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

Инвариант это бизнес-правило, которое всегда должно быть согласованным. Существуют различные виды согласованности. Одна из них это транзакционная, которая считается мгновенной и атомарной. Есть также конечная согласованность. При обсуждении инвариантов мы имеем в виду транзакционную согласованность. Мы можем иметь следующий инвариант:

c = a + b

Поэтому, когда, а = 2 и b = 3, с должно равняться 5. Согласно этому правилу, если с не равняется 5, то нарушается инвариант. Чтобы убедиться, что значение с согласовано, мы моделируем границу вокруг этих атрибутов модели.

AggregateType1 {    int a; int b; int c;    operations...}

Граница согласованности логически утверждает, что все, что находится внутри, должно придерживаться определенных бизнес-инвариантных правил независимо от того, какие операции выполняются. Согласованность всего, что находится за пределами этой границы, не имеет отношения к агрегату. Таким образом, агрегат является синонимом границы транзакционной согласованности.

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

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

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

Проектируйте небольшие агрегаты

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

Что произойдет, когда пользователь захочет добавить элемент бэклога в продукт, которому уже много лет и у которого уже тысячи таких элементов бэклога? Предположим, что в механизме персистентности доступна ленивая загрузка (lazy loading). Мы почти никогда не загружаем все элементы бэклога, релизы и спринты сразу. Тем не менее, тысячи элементов бэклога будут загружены в память, чтобы добавить еще один новый элемент в коллекцию. Хуже, если механизм персистентности не поддерживает ленивую загрузку. Иногда нам приходится загружать несколько коллекций, например, во время добавления элемента бэклога в релиз или в спринт. Все элементы бэклога, а также все релизы или все спринты будут загружены.

Чтобы увидеть это более наглядно, посмотрим на диаграмму на рисунке 3. Не позволяйте 0..* обмануть вас. Число ассоциаций почти никогда не будет равным нулю и будет постоянно расти с течением времени. Скорее всего, нам придется загружать тысячи и тысячи объектов в память одновременно для выполнения относительно простых операций. И это только для одного члена команды одного арендатора. Мы должны иметь в виду, что это подобная ситуация может произойти одновременно с сотнями и тысячами арендаторов, каждый из которых имеет несколько команд и множество продуктов. И со временем ситуация будет только ухудшаться.

Рис. 3. Модель Product. Несколько больших коллекций загружается во время множества простых операций.Рис. 3. Модель Product. Несколько больших коллекций загружается во время множества простых операций.

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

Если мы собираемся проектировать небольшие агрегаты, то нам необходимо выяснить, что значит небольшой. Крайним случаем будет агрегат с его глобальным идентификатором и одним дополнительным атрибутом, что не рекомендуется делать, если только это действительно не то, что требуется одному конкретному агрегату. Лучше будет, если ограничим агрегат только корневой сущностью (root entity), минимальным количеством атрибутов и/или объектов значений (object value).

Однако, какие именно данные (атрибуты, объекты значения) необходимы? Ответ прост: те, что должны иметь согласованность друг с другом. Например, Product имеет атрибуты name и description. Мы не можем представить эти атрибуты несогласованными, смоделированными в отдельных агрегатах. Если вы изменяете только один из этих атрибутов, то вероятно, потому что вы исправляете ошибку. Даже если эксперты предметной области не будут думать об этом как о явном бизнес-правиле, это неявное правило.

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

Однако иногда использования нескольких сущностей имеет смысл. Например, сумма заказа не должна превышать максимально допустимое значение. Из этого следует, что и сумма всех элементов заказа не должна превышать допустимое значение. Если заказ и элементы заказа будут находиться в разных агрегатах, то одновременное добавление элемента заказа несколькими пользователями может превысить этот лимит. В этом случае лучше объединить сущности Order и OrderItem в один агрегат. Но следует подчеркнуть, что в большинстве случаев инвариантами бизнес-моделей управлять проще, чем в этом примере. Признавая это, помогает нам моделировать агрегаты с как можно меньшим количеством свойств.

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

Не доверяйте каждому сценарию использования

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

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

Рис. 4. Изображен конкурентный доступ к данным между тремя пользователями. Они пытаются получить доступ к одним и тем же двум экземплярам агрегатов, что приводит к большому числу транзакционных сбоев.Рис. 4. Изображен конкурентный доступ к данным между тремя пользователями. Они пытаются получить доступ к одним и тем же двум экземплярам агрегатов, что приводит к большому числу транзакционных сбоев.

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

Просто потому, что дан сценарий использования, который требует поддержания согласованности в одной транзакции, не означает, что вы должны это делать. Часто в таких случаях бизнес-цель может быть достигнута с помощью конечной согласованности (eventual consistency) между агрегатами. Команда должна критически изучить сценарии использования и оспорить их предположения, особенно когда следование им в том виде, в каком они написаны, приведет к громоздким проектам. Команде, возможно, придется переписать сценарий использования. Новый вариант сценария использования будет указывать на конечную согласованность и приемлемую задержку обновления. Это один из вопросов, который будет рассматриваться во второй части.

Подробнее..

Перевод Эффективная конструкция агрегата. Заставляем агрегаты работать вместе

23.02.2021 10:12:21 | Автор: admin

Эта статья является конспектом материала Effective Aggregate DesignPart II: Making Aggregates Work Together.

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

Мы можем захотеть использовать композиционную структуру во время проектирования агрегатов. Такая структура позволит нам обходить глубокие графы объектов. Однако это не должно быть целью. DDD утверждает, что один агрегат может содержать ссылки на корень других агрегатов. Но это не значит то, что агрегаты входят в границу согласованности другого агрегата, который на них ссылается. Ссылка не приводит к образованию единого агрегата.

Рис. 1. Изображено два агрегата, а не один.Рис. 1. Изображено два агрегата, а не один.

На Java это выглядело бы следующим образом:

public class BacklogItem extends ConcurrencySafeEntity {  ...  private Product product;  ...}

BacklogItem содержит прямую связь с объектом Product.

Это имеет несколько последствий:

  • BacklogItem и Product не должны вместе изменяться в рамках одной транзакции, а только один из них.

  • Если изменяются несколько экземпляров агрегата в одной транзакции, это может свидетельствовать о нарушении границ согласованности и, следовательно, о ее неверности.

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

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

Ссылайтесь на другие агрегаты по идентификатору

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

Рис. 2. BacklogItem содержит связи с другими агрегатами за пределами своей границы с помощью идентификаторов.Рис. 2. BacklogItem содержит связи с другими агрегатами за пределами своей границы с помощью идентификаторов.
public class BacklogItem extends ConcurrencySafeEntity {  ...  private ProductId productId;  ...}

Таким образом, агрегаты без прямых ссылок на объекты (другие агрегаты) становятся меньше. Такая модель работает лучше, так как экземпляры требуют меньше времени для загрузки и занимают меньше памяти. Использование меньшего объема памяти имеет положительные последствия как для накладных расходов на выделение памяти, так и для сбора мусора.

Модель навигации

Ссылки по идентификатору полностью не исключают доступ к другим агрегатам. Можно использовать репозиторий изнутри агрегата для поиска. Такой метод называется автономной доменной моделью (disconnected domain model). Однако существуют другие рекомендуемые подходы. Используйте репозиторий или доменную службу для поиска зависимых объектов снаружи агрегата, то есть, например, в службах уровня приложения (application service).

public class ProductBacklogItemService ... {    ...    @Transactional    public void assignTeamMemberToTask( String aTenantId,        String aBacklogItemId, String aTaskId,        String aTeamMemberId) {        BacklogItem backlogItem = backlogItemRepository.backlogItemOfId(          new TenantId(aTenantId),          new BacklogItemId(aBacklogItemId)        );        Team ofTeam =        teamRepository.teamOfId( backlogItem.tenantId(), backlogItem.teamId());        backlogItem.assignTeamMemberToTask(          new TeamMemberId(aTeamMemberId), ofTeam,          new TaskId(aTaskId)        );      }      ...}

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

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

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

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

Используйте конечную согласованность за пределами границ

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

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

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

public class BacklogItem extends ConcurrencySafeEntity {  ...  public void commitTo(Sprint aSprint) {    ...    DomainEventPublisher    .instance()    .publish(      new BacklogItemCommitted(             this.tenantId(),             this.backlogItemId(),             this.sprintId()          )    );  }  ...}

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

Что произойдет, если подписчик столкнется с проблемой, которая приведет к сбою его транзакции? Модификацию можно повторить, если подписчик не подтвердил успешное выполнение операции. Механизм обмена сообщениями доставит повторно сообщение. Будет запущена новая транзакция и попытка выполнения необходимой команды. Этот процесс может продолжаться до тех пор, пока не будет достигнута согласованность, или пока не будет достигнут предел повторных попыток. Если произойдет полный отказ, может потребоваться компенсация, или, как минимум, сообщение об отказе.

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

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

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

Причины нарушения правил

Опытный специалист по DDD может иногда решить изменить несколько экземпляров агрегатов в одной транзакции, но только по уважительной причине. Какие же могут быть причины?

Причина первая: удобство пользовательского интерфейса

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

public class ProductBacklogItemService ... {    ...    @Transactional    public void planBatchOfProductBacklogItems(       String aTenantId, String productId,       BacklogItemDescription[] aDescriptions) {        Product product = productRepository.productOfId(          new TenantId(aTenantId), new ProductId(productId)        );        for (BacklogItemDescription desc : aDescriptions) {           BacklogItem plannedBacklogItem = product.planBacklogItem(             desc.summary(), desc.category(),             BacklogItemType.valueOf(desc.backlogItemType()),             StoryPoints.valueOf(desc.storyPoints())          );            backlogItemRepository.add(plannedBacklogItem);        }    }    ...}

Это вызывает проблемы с управлением инвариантов? В данном случае нет, поскольку не имеет значения, были ли они созданы по одному или сразу все вместе. Создаваемые экземпляры объектов являются агрегатами, которые сами поддерживают собственные инварианты. Таким образом, если создание нескольких экземпляров агрегата за один раз семантически ничем не отличается от многократного создания одного за раз, это является первой причиной нарушения правил.

Однако Уди Дахан рекомендует избегать создание сервисов приложения для пакетной обработки данных. Более подробно можно прочесть в оригинале.

Причина вторая: отсутствие технических инструментов

Конечная согласованность требует дополнительные инструменты для внепроцессорной обработки, таких как обмен сообщениями или таймеры. Что делать, если проект, над которым вы работаете, не предусматривает такого механизма? Хотя большинство из нас сочло бы это странным, но автор оригинала сталкивался именно с таким ограничением. Без механизма обмена сообщениями, без таймеров и другого, что можно было бы сделать?

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

Автор упомянул еще один фактор, который способствует нарушению правил - user-aggregate affinity. Я не до конца понял, о чем идет речь, поэтому не стал добавлять его в конспект. Если интересно, можно посмотреть в оригинале.

Причина третья: глобальные транзакции

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

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

Причина четвертая: производительность запросов

Бывают ситуации, когда все же лучше хранить прямые ссылки на другие объекты агрегата. Это может использоваться для улучшения производительности запросов в репозитории. Их необходимо тщательно взвесить с учетом возможных последствий для размера и общей производительности.

Вывод

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

Ссылки на все части

Подробнее..

Перевод Эффективная конструкция агрегатов. Понимание через исследование

28.02.2021 10:04:21 | Автор: admin

Эта статья является конспектом материала Effective Aggregate DesignPart III: Gaining Insight Through Discovery.

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

Переосмысление конструкции модели

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

Рис.1. Схема модели агрегата BacklogItemРис.1. Схема модели агрегата BacklogItem

Несмотря на положительный результат после первой итерации, некоторые опасения все же существуют. Например, атрибут story позволяет вмещать в себя большой объем текста. Из-за этого могут возникнуть возможные накладные расходы.

Учитывая эти потенциальные накладные расходы и ошибки, которые были допущены при проектировании большого кластерного агрегата, поставим перед собой цель уменьшить размер каждого агрегата в ограниченном контексте. Возникают некоторые вопросы. Существует ли истинный инвариант между BacklogItem и Task? Или эту связь можно разбить на две? Какова цена сохранения такой модели?

Ответ лежит в едином языке. Имеются следующие инварианты:

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

  • Когда член команды оценивает время в 0 часов, элемент бэклога проверяет все задачи на наличие оставшихся часов. Если их нет, то статус элемента бэклога автоматически меняется на done.

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

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

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

Оценка стоимости агрегата

Как показано на рисунке 1, каждый Task содержит коллекцию экземпляров EstimationLogEntry. Этот журнал фиксирует конкретные случаи, когда член команды выполняет новую оценку оставшихся часов. На практике, сколько элементов Task может содержать BacklogItem, и сколько элементов EstimationLogEntry будет содержать Task? Точно сказать сложно. Во многом это показатель того, насколько сложна задача и сколько будет длиться спринт. Но некоторые расчеты все же могут помочь.

Часы работы обычно пересчитываются каждый день после того, как член команды закончил работать над определенной задачей. Предположим, что большинство спринтов длится две или три недели. Давайте выберем количество дней от 10 до 15, например, пусть будет 12 дней.

Теперь рассмотрим количество часов, выделенных на каждую задачу. Обычно используют количество часов от 4 до 16. Часто, если задача превышает 12 часов, то эксперты Scrum предлагают разбить ее на более мелкие. В качестве теста предположим, что задачи оцениваются в 12 часов (1 час на каждый день спринта). Итак, получается 12 пересчетов для каждой задачи, предполагая, что каждая задача начинается с 12 часов, выделенные на нее.

Остается вопрос: сколько задач потребуется всего для одного элемента бэклога? Пусть будет, например, тоже 12 (я не стал расписывать, как автор пришел к такому числу; можно самому глянуть в оригинале). В итоге получается 12 задач, каждая из которых содержит 12 оценок в журнале, или 144 (12*12) на элемент бэклога. Хотя это может быть больше чем обычно, но это дает нам конкретную оценку для анализа.

Есть еще одно, что следует учесть. Если следовать рекомендациям экспертов Scrum по определению более мелких задач, это бы несколько изменило ситуацию. Удвоение числа задач (24) и уменьшение вдвое числа записей журнала (6) все равно дают 144. Однако это приведет к загрузке большего количества задач (24 вместо 12) во время запроса на оценку часов, потребляя при этом больше памяти. Но для начала давайте использовать 12 задач по 12 часов каждая.

Общие сценарии использования

Теперь важно рассмотреть общие сценарии использования. Как часто в одном пользовательском запросе нужно будет загружать в память все 144 объекта одновременно? Произойдет ли вообще это когда-то? Если нет, то каково максимальное количество объектов? Кроме того, будет ли многопользовательские запросы, которые могут привести к транзакционной конкуренции? Давайте посмотрим.

Каждая сущность имеет атрибут версии для оптимистической блокировки. Это имеет смысл поскольку инвариант изменения статуса управляется корневой сущностью BacklogItem. Когда статус автоматически изменяется, версия BacklogItem тоже изменяется. Таким образом, изменения в задачах могут происходить независимо друг от друга и не затрагивать корень при каждом изменении, если это не приводит к изменению статуса.

Однако следует учитывать, что если используется документноориентированное хранилище, а не РСУБД, то корень будет изменяться каждый раз, когда изменяется задача, так как она является частью документа.

Когда элемент бэклога впервые был создан, в нем нет задач. Обычно задачи определяются после планирования спринта. Во время совещания между членами команды определяются какие задачи необходимо добавить. В процессе обсуждения каждый член команды добавляет задачу в соответствующий элемент бэклога. Вроде как нет никакой необходимости параллельной работы нескольких членов команды с агрегатом BacklogItem. Это вызывало бы транзакционную гонку, и один из двух запросов потерпел бы неудачу.

Если бы команда узнала, что несколько пользователей регулярно хотят добавлять задачи вместе, это значительно изменило бы анализ. И чаша весов склонилась бы в пользу разделения BacklogItem и Task на два отдельных агрегата. Однако можно было бы отключить оптимистическую блокировку. В этом случае имеет смысл разрешить одновременное добавление задач, если они не создают проблем с производительностью и масштабируемостью.

Если задачи сначала оцениваются в 0 часов, а затем оценка уточняется и обновляется, транзакционной конкуренции все равно не будет. Одновременное использование в данном случае не меняет статус элемента бэклога, поскольку статус может смениться на done, если оценка меняется с ненулевого значения на ноль, или откатывается, если статус уже done, и часы меняются с нулевого значения на положительное.

Будут ли ежедневные оценки приводить к проблемам? В первый день спринта обычно нет журналов оценки по заданной задаче элемента бэклога. В конце первого дня каждый член команды, работающий над задачей, сокращает расчетное количество часов на один. Это добавляет новую запись в журнал оценки к каждой задаче, но статус элемента бэклога не изменяется. При этом только один член команды корректирует часы определенной задачи. Только на 12-й день происходит изменение статуса. После того, как будет добавлена последняя 144 запись в журнал для 12 задаче, происходит автоматический переход статуса в done.

Этот анализ привел к важному выводу. Даже если изменить сценарии использования, ускорив выполнение задач вдвое (6 дней), это ничего не меняет. Версия корня меняется только при окончательной оценке, которая меняет статус. Это кажется безопасным, хотя накладные расходы на память все еще под вопросом.

Потребление памяти

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

Что насчет общего количества задач и оценок в памяти во время каждого повторного оценивания? При использовании ленивой загрузки для задач и журналов оценки у нас будет до 12 + 12 объектов в памяти во время одного запроса, поскольку все 12 задач будут загружены во время обращения к этой коллекции. Чтобы добавить последнюю запись в журнал оценки к одной из задач, нужно загрузить коллекцию записей журнала и это дает еще до 12 объектов. В конечном итоге агрегат содержит один элемент бэклога, до 12 задач и до 12 записей в журнале, что в сумме дает максимум 25 объектов. Это не очень много. Другой факт заключается в том, что максимальное количество объектов не достигается до последнего дня спринта. В течение большей части спринта агрегат будет еще меньше.

Вызовет ли это проблему с производительностью? Необходимо провести тесты, чтобы узнать возможные накладные расходы многократной выборки.

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

Альтернативная конструкция

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

Рис. 2. BacklogItem и Task как отдельные агрегатыРис. 2. BacklogItem и Task как отдельные агрегаты

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

Реализация конечной согласованности

Когда Task выполняет команду estimateHoursRemaining(), она публикует соответствующие доменное событие для достижения конечной согласованности. Событие имеет следующие свойства:

public class TaskHoursRemainingEstimated implements DomainEvent {     private Date occurredOn;    private TenantId tenantId;    private BacklogItemId backlogItemId;     private TaskId taskId;    private int hoursRemaining;    ...}

Теперь определенный подписчик будет прослушивать это событие и делегировать доменной службе выполнение согласованности. Служба:

  • Использует BacklogItemRepository для получения BacklogItem по идентификатору.

  • Использует TaskRepository для получения всех экземпляров Task, связанных с конкретным BacklogItem

  • Выполняет BacklogItem команду estimateTaskHoursRemaining(), передавав ей значение hoursRemaining и определенный экземпляр Task. BacklogItem может менять свой статус в зависимости от параметров.

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

public class TaskRepositoryImpl implements TaskRepository {    ...    public int totalBacklogItemTaskHoursRemaining(       TenantId aTenantId,        BacklogItemId aBacklogItemId) {            Query query = session.createQuery(            "select sum(task.hoursRemaining) from Task task "            + "where task.tenantId = ? and "            + "task.backlogItemId = ?");            ...    }}

Конечная согласованность немного усложняет пользовательский интерфейс. Если согласование состояния не произойдет в течение нескольких сотен миллисекунд, как пользовательский интерфейс отобразит новое состояние? Необходимо ли размещать бизнес-логику в представлении для определения текущего статуса? Это был бы антипаттерн умный UI. В противном случае представление отобразит устаревший статус. Это легко может быть воспринято как ошибка.

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

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

Время принимать решение

Исходя из всего этого анализа, возможно будет лучше отказаться от разделения Task и BacklogItem. Сейчас оно не стоит дополнительных усилий, риска нарушения истинного инварианта или возможности столкнутся с устаревшим статусом в представлении. Текущий агрегат довольно мал. Даже если в худшем случае будет загружено 50 объектов, а не 25, это все равно кластер небольшого размера.

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

Вывод

Используя модель предметной области на реальном примере, мы рассмотрели насколько важно следовать эмпирическим правилам при проектировании агрегатов:

  • Моделируйте истинные инварианты в границах согласованности.

  • Проектируйте небольшие агрегаты.

  • Ссылайтесь на другие агрегаты по идентификатору.

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

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

Ссылки на все части

Подробнее..

Перевод Погружение в CQRS

03.03.2021 10:13:29 | Автор: admin

Эта статья является конспектом материала Clarified CQRS.

Прежде чем начать разбираться с CQRS, нужно понять две основные движущие силы, стоящие за ним: сотрудничество и устаревание.

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

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

Стандартная многоуровневая архитектура не решает ни одну из этих проблем. Хотя размещение всех данных в одной БД может способствовать улучшению совместной работе, устаревание обычно усугубляется в такой архитектуре из-за использования кеширования в качестве средства повышения производительности.

Рис.1. Модель CQRSРис.1. Модель CQRS

Компоненты на рисунке с названием AC являются автономными системами. Позже будет описано, что делает их автономными во время обсуждения команд (Commands CQRS). Но для начала давайте разберемся с запросами (Queries CQRS)

Запросы (Queries)

Если данные, которые собираемся показывать пользователям, все равно устаревшие, нужно ли идти в основную БД и получать их из нее? Зачем преобразовывать эти структуры в доменные объекты, если нам нужны только данные, а не поведение?

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

Как насчет того, чтобы создать дополнительное хранилище данных, данные которого могут быть немного не синхронизированы с основной БД имеется в виду, что данные, которые показываются пользователям, все равно устарели, так почему бы не отразить их в самом хранилище данных?

Какая же будет структура такого хранилища данных? Как на счет по одной таблице для каждого представления? Данные формируются с помощью одного запроса SELECT * FROM MyViewTable и передаются пользователю на экран. Это было бы максимально просто. Можно при необходимости это обернуть тонким фасадом. В итоге данные для представления уже будут готовы и не нужно преобразовывать их во что-то другое (например, в доменные объекты).

Хранилище данных для запросов

В качестве хранилища данных для запросов можно использовать текущую БД, но это не единственный вариант. Нужно учесть, что схема запросов идентична модели представления. Нет никаких связей между различными моделями представления, поэтому и не нужны никакие связи между таблицами в БД для запросов. Это указывает на то, что использовать реляционную БД необязательно в данном случае.

Масштабирование запросов

Если в итоге запросы выполняются из отдельного хранилища данных, а не из главной БД, можно легко добавить больше экземпляров этих хранилищ. Тот же механизм, который обновляет один экземпляр, может использоваться для многих экземпляров.

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

Модификация данных

Поскольку наши пользователи принимают решение на основе устаревших данных, нам нужно быть осторожными с тем, какие вещи мы пропускаем.

Допустим, у нас есть представитель службы поддержки клиентов, который разговаривает по телефону с клиентом. Этот пользователь смотрит на данные клиента на экране и хочет их изменить (адрес, ФИО и др.). Однако пользователь не знает, что после отображения данных на экране произошло некое событие. Из отдела выставления счетов пришло уведомление, что этот же клиент не оплачивает свои счета они просроченные. На данном этапе пользователь отправляет данные для изменения. Стоит ли принимать эти изменения?

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

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

Чтобы предоставить правильный уровень детализации данных и избежать конфликтов используются команды (CQRS).

Команды (Commands)

Ключевым элементом CQRS является переосмысление дизайна пользовательского интерфейса, чтобы избежать конфликтов конкурентного доступа к данным. Использование пользовательского интерфейса, подобного Excel (как я понял, автор оригинала имел в виду сущности с большим количеством полей денормализованные данные), для изменения данных не подходит.

Можно бы даже подумать о том, чтобы разрешить пользователям отправлять новую команду еще до того, как они получат подтверждение предыдущей. Можно иметь небольшой виджет сбоку, показывающий пользователю ожидающие команды, проверяя их асинхронно. Когда получаем подтверждение от сервера, помечаем их соответствующим образом (выполнилась успешно или нет). Затем пользователь может щелкнуть на неудачную команду, чтобы увидеть причину сбоя.

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

Команды и валидация

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

В раннем примере данные, представленные нашим представителем службы поддержки клиентов, были действительными. Их отклонение произошло только из-за того, что пришло событие биллинга, которое потребовало отклонения команды. Если бы это событие биллинга не произошло, данные были бы приняты. Это указывает на то, что даже, если команда валидная, все равно могут быть причины для ее отклонения.

Таким образом, даже если клиент выполняет проверку данных перед их отправкой, сервер все равно будет проверять все входящие команды, не доверяя клиенту.

Переосмысление UIs и команды с точки зрения валидации

Клиент может использовать хранилище запросов (Queries) для проверки команд. Например, перед отправкой команды, мы можем проверить, существует ли название улицы в хранилище запросов.

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

Таким образом, на стороне сервера будет только одна причина, по которой такая команда не выполнится проблема согласованности. Кто-то может удалить эту улицу и хранилище запросов не успеет обновиться.

Причины сбоя валидных команд и что с этим делать

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

В приведенном выше примере отклонение происходит только потому, что событие биллинга пришло первым. Но первый может быть за миллисекунду до команды. Что, если пользователь нажал кнопку на миллисекунду раньше? Должно ли это на самом деле изменить бизнес-результат?

Если событие биллинга пришло вторым, то определенные данные пользователя, например, статус, должны измениться. Также отправится уведомление на почту. В таком случае почему бы не сделать также, если уведомление биллинга пришло первым? И если уже есть настроенная модель уведомлений, действительно ли нужно возвращать ошибку?

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

Команды и автономность

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

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

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

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

Автономные компоненты

Хотя на рисунке 1 мы видим, что все команды передаются в один и тот же AC, мы могли бы обработать каждую команду другим AC, у каждой из которых своя очередь. Это дало бы нам представление о том, какая очередь является самой большой, что позволит легко увидеть, какая часть системы является узким местом. И можно будет добавить дополнительные узлы обработки очередей, чтобы масштабировать такие части системы, которые работают медленно.

Уровень обслуживания (service layer)

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

Смотря на уровень обслуживания с точки зрения команд мы видим, что объекты обрабатываются различными командами. Каждая команда не зависит от другой, так почему мы должны позволять объектам, которые их обрабатывают, зависеть друг от друга? Зависимости это вещи, которых следует избегать, если для них нет веской причины.

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

Где доменная модель?

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

Еще одна вещь, которую следует понять это то, что доменная модель не использует запросы (CQRS). Итак, вопрос в том, зачем нужно иметь так много связей между сущностями в доменной модели?

Действительно ли нам нужна коллекция заказов для сущности клиент? В какой команде нам нужно перемещаться по этой коллекции? Нужно ли в самом деле для команды отношение один ко многим?

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

Таким образом, команды могут быть полностью обработаны одной сущностью корнем агрегата, который является границей согласованности.

Персистентность для обработки команд

Учитывая, что БД, используемая для обработки команд, не используется для запросов и что большинство команд содержат идентификаторы строк, на которые они будут влиять, действительно ли нужен столбец для каждого отдельного свойства объекта домена? Что, если просто сериализовать доменную сущность и поместить ее в один столбец, а другой столбец будет содержать идентификатор? Это звучит очень похоже на хранилище key-value. В таком случае действительно ли нужно объектно-реляционное преобразование данных? Также можно выделить дополнительные свойства для каждого фрагмента данных, которое требует обеспечения уникальности.

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

Синхронизация хранилища запросов

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

MakeCustomerPerferredCommand CustomerHasBeenMadePerferredEvent

Публикация события выполняется транзакционно вместе с обработкой команды и изменениями в БД. Таким образом, любой сбой фиксации приведет к тому, что событие не будет отправлено.

Автономный компонент, который обрабатывает эти события и обновляет хранилище данных запросов, довольно прост. Он преобразует данные события в данные модели представления.

Ограниченный контекст

Хотя CQRS затрагивает многие части архитектуры ПО, он все еще не находится на вершине пищевой цепочки. Если используется CQRS, то в ограниченном контексте или бизнес-компоненте (SOA), который является частью предметной области. На события, публикуемые одним бизнес-компонентом, подписываются другие бизнес-компоненты, каждый из которых обновляет свои хранилища запросов и команд по мере необходимости.

Пользовательский интерфейс, находящийся в каждом бизнес-компоненте, может быть собран в одно приложение, которое предоставляет пользователям единое составное представление обо всех частях предметной области.

Вывод

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

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

Хотя вследствии применения CQRS в проекте приведет к более удобному обслуживанию и повышению производительности, эта простота и масштабируемость требует понимание подробных бизнес-требований.

Подробнее..

Разложение монолита

21.03.2021 10:06:16 | Автор: admin

Эта статья является конспектом книги От монолита к микросервисам. Материал статьи посвящен шаблонам разложения монолита.

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

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

Вырезать, скопировать или заново реализовать?

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

Рефакторинг монолита

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

Модульный монолит?

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

Шаблон: приложение Фикус-удавка

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

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

Реализация шаблона Фикус-удавка основана на трех шагах (рис. 1). Для начала нужно определить части существующей системы, которые хотите мигрировать. Затем нужно реализовать эту функциональность в новой микрослужбе. Когда новая реализация будет готова, нужно иметь возможность перенаправить вызовы из монолита в новую микрослужбу.

Рис.1 Общий вид шаблона Фикус-удавкаРис.1 Общий вид шаблона Фикус-удавка

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

Шаблон выгоден, когда над существующим монолитом работают другие люди, так как он помогает уменьшить конкуренцию. Он также очень полезен, когда монолит является черно-ящичной системой, например, сторонний софт или служба SaaS.

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

Рис.2 - Шаблон "Фикус-удавка" работает не слишком хорошо, когда функциональность, которую нужно перенести, находится глубже внутри существующей системыРис.2 - Шаблон "Фикус-удавка" работает не слишком хорошо, когда функциональность, которую нужно перенести, находится глубже внутри существующей системы

Также нужно будет учесть природу вызовов, осуществляемых внутрь существующей системы. Такой протокол, как HTTP, хорошоподдается перенаправлению.В сам HTTP встроены концепции прозрачного перенаправления, и прокси-селекторы используются для четкого понимания природы входящего запроса и егосоответствующей переадресации. Другие типы протоколов, такие, как некоторыеRPC, поддаются перенаправлению хуже. Чем больше работы нужно выполнить напрокси-слое, для того чтобы понять и потенциально трансформировать входящийвызов, тем менее жизнеспособным становится этот вариант.Несмотря на эти ограничения, приложение Фикус-удавка снова и снова доказывает, что это очень полезный метод миграции.

Пример: обратный прокси-селектор HTTP

На рис.3 изображен существующий монолит, который выставляет наружу HTTP-интерфейс. Цель: вставить обратный прокси-селектор HTTP между вышестоящими вызовами и нижестоящим монолитом.

Рис. 3 - Простой общий вид монолита до реализации "удавки"Рис. 3 - Простой общий вид монолита до реализации "удавки"

Шаг 1 если еще нет прокси-селектора, то необходимо его добавить. На этом первом шаге прокси-селектор просто позволит любымвызовам проходить насквозь без изменений.

Шаг 2 мигрировать функциональность. После установки нашего прокси-селектора на свое место можно начать извлечениеновой микрослужбы. Сам этот шаг можно разбить на несколько этапов. Прежде всего, привести базовуюслужбу в рабочее состояние без реализации какой-либо функциональности.Служба должна будет принимать вызовы соответствующей функциональности, но на этом этапе можно просто возвращать код ошибки 501 Not Implemented. Даже на этом шаге можно развернул службу в производственнойсреде. Это позволит освоиться с процессом развертывания в производстве ипротестировать службу прямо на месте.

Шаг 3 перенаправить вызовы. Только после завершения переноса всей функциональности, нужно переконфигурировать прокси-селектор с целью перенаправления вызовов, как мы видим на рис. 4.Если по какой-либо причине это не получается, то вы можете выставить перенаправление в прежнее состояние для большинства прокси-селекторов это оченьбыстрый и легкий процесс, дающий быстрый откат.

Рис. 4. Шаг 3: перенаправление вызова функциональности "Расчета заработной платы" в завершение миграцииРис. 4. Шаг 3: перенаправление вызова функциональности "Расчета заработной платы" в завершение миграции

Пример: перехват сообщений

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

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

Рис. 5 Монолит, принимающий вызовы через очередьРис. 5 Монолит, принимающий вызовы через очередь

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

Альтернативный подход - изменить монолит и побудить его игнорировать отправляемые сообщения, которые должны быть получены новой службой, как мывидим на рис. 6. Здесь и новая служба, и монолит делят между собойодну и ту же очередь, и локально они используют процесс своего рода сопоставления с шаблоном для прослушивания сообщений, в которых они заинтересованы.

Рис. 6 - Вариант использования маршрутизатора, основанного на содержимом, для перехвата вызовов, связанных с обменом сообщениямиРис. 6 - Вариант использования маршрутизатора, основанного на содержимом, для перехвата вызовов, связанных с обменом сообщениями

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

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

Шаблон: Ветвление по абстракции

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

Он состоит из пяти шагов:

  1. Создать абстракцию для заменяемой функциональности.

  2. Изменить клиентов существующей функциональности так, чтобы они использовали новую абстракцию.

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

  4. Переключиться на новую реализацию.

  5. Очистить абстракцию и удалить старую реализацию.

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

Шаг 2 использовать абстракцию. Теперь, когда наша абстракция создана, нам нужно изменить существующих клиентов функциональности "Уведомления", для того чтобы использовать этуновую точку абстракции, как мы видим на рис. 7.

Рис. 7 - Шаг 2: изменить существующих клиентов так,чтобы они использовали новую абстракциюРис. 7 - Шаг 2: изменить существующих клиентов так,чтобы они использовали новую абстракцию

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

Рис. 8 - Шаг 3: Создать новую реализацию абстракцииРис. 8 - Шаг 3: Создать новую реализацию абстракции

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

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

Шаблон: Параллельное выполнение

И шаблон Фикус-удавка, и шаблон Ветвление по абстракции позволяют старыми новым реализациям одной, и той же функциональности одновременно сосуществовать в производстве. Для снижения риска переключения на новую реализацию, основанную на службах, эти методы дают возможность быстро переключаться назадк предыдущей реализации.

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

Далее будет описан пример на основе сравнения ценообразования кредитных деривативов. Автор книги участвовал в проекте по изменению платформы, используемой для расчетов типа финансового продукта, именуемого кредитными деривативами. Из-за больших рисков они приняли решение выполнять два набора вычислений бок о бок и проводить
ежедневные сравнения результатов. События ценообразования запускались с помощью событий, которые легко дублировались таким образом, чтобы обе системы выполняли расчеты, как видим на рис. 9. Вследствие таких сравнений были обнаружены проблемы и дефекты в существующей системе.

Рис. 9 - Пример параллельного выполненияРис. 9 - Пример параллельного выполнения

В конце концов, они пришли к использованию новой системы в качестве источника истины для расчетов, а некоторое время спустя вывели старуюсистему из эксплуатации.

Стоит отметить, что параллельное выполнение отличается от выпуска канареечного релиза. Канареечный релиз (canary
release) связан с направлением некоторого подмножества пользователейк новой функциональности, при этом подавляющая часть пользователей видит старую реализацию. Идея состоит в том, что если новая система имеет проблему,то только подмножество запросов подвержено влиянию этой проблемы.Еще один родственный метод называется темным запуском (dark launching). При темном запуске развертывается новая функциональность и тестируется, ноновая функциональность для пользователей невидима. Поэтому параллельное выполнение является способом реализации темного запуска, посколькуновая функциональность практически невидима для пользователей до техпор, пока вы не переключитесь на нее.Темный запуск, параллельные выполнения и выпуск канареечных релизов все эти методы можно использовать для верификации того, что новая функциональность работает правильно, и для уменьшения влияния, если окажется, чтоэто не так.

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

Шаблон: Сотрудник-декоратор

Что произойдет, если вы захотите вызвать какое-то поведение, основываясь на том,что происходит внутри монолита, но вы неспособны изменить сам монолит? Шаблон Сотрудник-декоратор (decorating collaborator) окажет здесь большую помощь.Широко известный структурный шаблон Декоратор позволяет прикреплять новую функциональность к чему-либо без того, чтобы лежащая в основании вещьчто-то об этом знала. Мы собираемся использовать декоратор, чтобысделать вид, что наш монолит делает вызовы нашей службы напрямую, даже еслимы на самом деле не изменили лежащий в основании монолит.Вместо перехвата этих вызовов до того, как они достигнут монолита, мы даем вызову выполняться как обычно. Затем, основываясь на результате этого вызова, мыобращаемся к нашим внешним микрослужбам. Давайте рассмотрим эту идею на примерефункционала программы лояльности для компании Music Corp (это выдуманная компания для иллюстрации концепций из другой книги автора Building Microservices)

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

Рис. 10 - Когда заказ успешно размещен, наш прокси-селектор обращается к службе Лояльности для начисления баллов клиентуРис. 10 - Когда заказ успешно размещен, наш прокси-селектор обращается к службе Лояльности для начисления баллов клиенту

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

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

Шаблон: Захват изменений в данных

В рамках шаблона Захват изменений в данных (change data capture), вместо тогочтобы пытаться перехватывать вызовы в монолит, мы реагируемна изменения, вносимые в хранилище данных.

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

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

Рис. 11 - Применение захвата изменений в данных для вызова новой службы печатиРис. 11 - Применение захвата изменений в данных для вызова новой службы печати

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

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

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

Пакетное копирование изменений. Вероятно, наиболее упрощенный подход написание программы, которая на регулярной основе сканирует затрагиваемую базу данных на предмет того, какиеданные изменились, и копирует эти данные в место назначения, например, с помощью cron. Главная проблема выяснить, какие данные фактически изменились с моментапоследнего запуска пакетного копирования. Некоторые БД позволяют просматривать метаданные таблиц, чтобы увидеть, когда части базы данных изменились, но этот подходдалеко не универсален и будет давать вам временные метки изменений только науровне таблицы, когда предпочтительней бы иметь информацию на уровне строк. Вымогли бы начать добавлять эти временные метки сами, но в результате объем работы значительно возрастет.

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

Вывод

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

Подробнее..

Перевод Как справиться с более чем двумя миллиардами записей в SQL-базе данных

22.03.2021 18:15:22 | Автор: admin

В рамках набора группы учащихся на курс "Highload Architect" подготовили перевод интересной статьи.

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


У одного из наших клиентов возникла проблема с большой, постоянно растущей, таблицей в MySQL с более чем 2 миллиардами записей. Без модернизации инфраструктуры была опасность исчерпания дискового пространства, что потенциально могло сломать все приложение. С такой большой таблицей были и другие проблемы: низкая производительность запросов, плохая схема, и, из-за огромного количества записей, не было простого способа анализировать эти данные. Также нам нужно было решить эти проблемы без простоев в работе приложения.

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

Спасение в облаках

После оценки нескольких альтернативных решений мы решили отправлять данные в какое-нибудь облачное хранилище. И наш выбор пал на Google Big Query. Мы выбрали его, потому что клиент предпочитал облачные решения от Google, а также данные были структурированными, предназначались для аналитики и нам не требовалась низкая задержка передачи данных (low latency). Поэтому BigQuery, казалась, идеальным решением (см. диаграмму ниже).

После тестов, о которых вы можете прочитать в посте Анджея Людвиковски (Andrzej Ludwikowski), мы убедились, что Big Query достаточно хорошее решение, отвечающее потребностям наших клиентов и легко позволяет использовать аналитические инструменты для анализа данных. Но, как вы, возможно, уже знаете, большое количество запросов в BigQuery может привести к увеличению стоимости, поэтому мы хотели избежать запросов в BigQuery напрямую из приложения и использовать его только для аналитики и как что-то вроде резервной копии.

https://cloud.google.com/solutions/infrastructure-options-for-data-pipelines-in-advertising#storing_data

Передача данных в облако

Для передачи потока данных есть много разных способов, но наш выбор был очень прост. Мы использовали Apache Kafka просто потому, что она уже широко использовалась в проекте и не было смысла внедрять другое решение. Использование Kafka дало нам еще одно преимущество мы могли передавать все данные в Kafka и хранить их там в течение необходимого времени, а затем использовать для миграции в выбранное решение, которое справилось бы со всеми проблемами без большой нагрузки на MySQL. С таким подходом мы подготовили себе запасной вариант в случае проблем с BigQuery, например, слишком высокой стоимости или сложностей и с выполнением необходимых запросов. Как вы увидите ниже, это было важное решение, которое дало нам много преимуществ без каких-то серьезных накладных расходов.

Потоковая передача из MySQL

Итак, когда речь заходит о передаче потока данных из MySQL в Kafka, вы, вероятно, думаете о Debezium или Kafka Connect. Оба решения отличный выбор, но в нашем случае не было возможности их использовать. Версия сервера MySQL была настолько старой, что Debezium ее не поддерживал, а обновление MySQL было невозможным. Мы также не могли использовать Kafka Connect из-за отсутствия автоинкрементного столбца в таблице, который мог бы использоваться коннектором для запроса новых записей без потери каких-либо из них. Мы знали, что можно использовать timestamp-столбцы, но при этом подходе могли быть потери строк из-за того, что запрос использовал более низкую точность timestamp, чем указано в определении столбца.

Конечно, оба решения хороши, и если нет никаких препятствий для их использования, то я могу рекомендовать их для передачи данных из вашей базы данных в Kafka. В нашем случае нам нужно было разработать простого Kafka Producer, который запрашивал данные без потери каких-либо записей и передавал их в Kafka. И Kafka Consumer, отправляющего данные в BigQuery, как показано на диаграмме ниже.

Отправка данных в BigQueryОтправка данных в BigQuery

Секционирование как способ экономии места

Итак, мы отправили все данные в Kafka (сжимая их для уменьшения полезной нагрузки), а затем в BigQuery. Это помогло нам решить проблемы с производительностью запросов и быстро анализировать большой объем данных. Но осталась проблема с доступным местом. Мы хотели найти решение с заделом на будущее, которое справилось бы с проблемой сейчас и могло быть легко использовано в будущем. Мы начали с разработки новой таблицы. Мы использовали serial id в качестве первичного ключа и секционирование по месяцам. Секционирование этой большой таблицы дало нам возможность создавать резервные копии старых секций и усекать (truncate) / удалять (drop) их, чтобы освободить место, когда секция больше не нужна. Итак, мы создали новую таблицу с новой схемой и использовали данные из Kafka для ее заполнения. После переноса всех записей мы развернули новую версию приложения, которая для INSERT использовала новую таблицу с секционированием и удалили старую, чтобы освободить место. Конечно, вам понадобится достаточно свободного места для переноса старых данных в новую таблицу, но в нашем случае во время миграции мы постоянно делали резервные копии и удаляли старые разделы, чтобы быть уверенными, что у нас хватит места для новых данных.

Передача данных в секционированную таблицуПередача данных в секционированную таблицу

Сжатие данных как еще один способ освободить пространство

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

Одна из идей была посмотреть, как различные данные распределены по таблице. После нескольких запросов выяснилось, что почти 90% данных никому не нужны. Поэтому мы решили их сжать, написав Kafka Consumer, который отфильтровал бы ненужные записи и вставлял только нужные в еще одну таблицу. Назовем ее сжатой таблицей (compacted table), что показано на приведенной ниже диаграмме.

После сжатия (строки со значением "A" и "B" в колонке type были отфильтрованы во время миграции).

Передача данных в compacted-таблицуПередача данных в compacted-таблицу

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

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

Так как мы используем BigQuery только для аналитических запросов, а остальные запросы, отправляемые пользователями через приложение, по-прежнему выполняются в MySQL, то затраты оказались не такие и большие, как можно было бы ожидать. Еще одна важная деталь все было выполнено без простоев, ни один клиент не пострадал.

Резюме

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


Узнать подробнее о курсе "Highload Architect".

Смотреть вебинар на тему Выбор архитектурного стиля.

Подробнее..

Разложение монолита Декомпозиция БД (часть 2)

28.03.2021 10:07:00 | Автор: admin

Эта статья является заключительным конспектом книги От монолита к микросервисам. Материал статьи посвящен декомпозиции БД во время процесса разложения монолита на микросервисы.

Извлечение микрослужбы не делается до техпор, пока код приложения не будет работать в его собственной службе, а данные,которыми он управляет, не будут извлечены в его собственную логически изолированную БД. Однако в какой последовательности извлечения должны выполняться?

Есть несколько вариантов:

  • Сначала выделить БД, а затем код.

  • Сначала выделить код, а затем БД.

  • Выделить сразу все.

У каждого варианта есть свои плюсы и минусы. Далее будут рассмотрены эти варианты.

Сначала выделить БД

С помощью отдельной схемы потенциально увеличивается число вызововБД для выполнения одного-единственного действия. Если раньше были все необходимые данные в одной инструкции select, то теперь потребуется вытаскивать данные из двух мест и соединять их в памяти. Кроме того, мы в итоге нарушаем транзакционную целостность, когда переходим к двум схемам, что окажет значительное влияние на приложения. Выделяя схемы, но держакод приложения вместе мы даем себе возможность откатывать наши изменения назад или продолжать корректировать вещи, не влияя накаких-либо потребителей нашей службы, если мы понимаем, что идем по неправильному пути. Как только мы убедимся, что выделение БД имеет смысл, то можемподумать о разделении кода приложения на две службы.

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

Одна БД на один ограниченный контекст

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

Рис. 1 - Каждый ограниченный контекст в службе валового дохода имелсвою собственную отдельную схему базы данных, допускающую разложение в дальнейшем.Рис. 1 - Каждый ограниченный контекст в службе валового дохода имелсвою собственную отдельную схему базы данных, допускающую разложение в дальнейшем.

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

Сначала выделить код

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

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

Шаблон: Монолит как слой доступа к данным

Вместо прямого доступа к данным из монолита мы можем просто перейти к модели, в которой создаем API в самом монолите. Счет-фактура требуется информация о сотрудниках, которая хранится в БД монолита, поэтому мы создаем API Сотрудники, позволяющий службе Счет-фактура к ним обращаться.

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

На этот шаблон можно смотреть, как на способ выявления других потенциальных служб. Развивая эту идею, мы могли увидеть, что API Сотрудники выделяется из монолита, становясь микрослужбой самой по себе, как показано нарис. 2.

Рис. 2 - Использование API Сотрудники для выявления контура службы Сотрудники, которая должна быть выделена из монолита.Рис. 2 - Использование API Сотрудники для выявления контура службы Сотрудники, которая должна быть выделена из монолита.

Указанный шаблон лучше всего работает, когда код, управляющий данными, по-прежнему находится в монолите, то есть переход данных из одного состояния в другое обеспечивается именно монолитом. Поэтому из этого следует, что микрослужба, которая хочет обратиться к этому состоянию (или изменить его), должна обращаться к монолиту.Если данные, к которым пытаетесь обратиться, в БД монолита, но на самом деле должны быть во владении микрослужбы, то лучше пропустить этот шаблон и вместо него выделить данные.

Выделить БД и код вместе

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

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

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

Шаблон: Разложить таблицу

Бывает ситуация, когда данные в одной таблице нуждаются в разделение для двух и более микрослужб. На рис. 3 видим одну совместную таблицу Товарная позиция, в которой хранится информация для каталога и склада.

Рис. 3 - Одна таблица, соединяющая в себе два выделяемых ограниченных контекста.Рис. 3 - Одна таблица, соединяющая в себе два выделяемых ограниченных контекста.

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

Этот пример довольно прямолинейный, отделить владение данными, один столбецза другим относительно легко. Но что произойдет, когда несколько частей кода обновляютодин и тот же столбец? Например, имеется таблица Клиент, которая содержитстолбец Статус.

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

Это означает, что после выделения служб новая служба Финансы будет нуждаться в вызове службы для обновления этого статуса, как мы видим на рис. 4.

Рис. 4 - Новая служба "Финансы" должна совершать вызовы службы для приостановки клиента.Рис. 4 - Новая служба "Финансы" должна совершать вызовы службы для приостановки клиента.

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

Шаблон: Перенос связи по внешнему ключу в код

Допустим мы решили извлечь службу Каталог, которая может управлять и предоставлять информацию об исполнителях, треках и альбомах. В настоящее время каталожный программный код внутри монолита хранит информацию о компакт-дисках в таблице Альбомы. В таблице Приходно-расходный регистр хранится информация о продажах (рис. 5). Таблица Приходно-расходный регистр просто регистрирует сумму за компакт-диск, вместе с идентификатором, который ссылается на проданную товарнуюпозицию. Идентификатор в примере называется SKU.

Рис. 5 Связь по внешнему ключу.Рис. 5 Связь по внешнему ключу.

В конце каждого месяца нужно составлять отчет с описанием самыхпродаваемых компакт-дисков. Таблица Приходно-расходный регистр помогаетнам понять, копий какого артикула продано больше всего, но информация об этомартикуле находится в таблице Альбомы. Для отчетности нужен запрос к БД, инициированный финансовым кодом, который должен присоединить информацию из таблицы Приходно-расходный регистр к таблице Альбомы, как показано на рис. 5. В итоге, имеется связь по внешнему ключу между таблицами.

Мы хотим выделить каталожный и финансовый код в собственные службы, и это означает, что данные должны идти тоже. Но что произойдет со связью по внешнему ключу? Нужно подумать о двух ключевых проблемах. Во-первых, во время генерации отчета в будущем, как служба Финансы будетизвлекать каталожную информацию, если не сможет этого сделать через существующую в БД операцию соединения? Во-вторых, что мы сделаем с темфактом, что теперь будет несогласованностьданных?

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

Более запутанная проблема возможность столкнуться с несогласованностьюданных в случае отдельных служб с отдельными схемами.С одной схемой можно удалить строку в таблице Альбомы, если бы в таблице Приходно-расходный регистр была ссылка на эту строку. Схема обеспечила бы согласованность данных. Какие у нас тут варианты?

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

Более приемлемый вариант предоставить службе Финансы возможность разбираться с тем фактом, что служба Каталог может не иметь информации об альбоме, делая это следующим способом: показать в отчетесообщение Информация об альбоме отсутствует, если мы не можем найти данный артикул.

Один из способов уменьшения несогласованности в системе мог бы заключатьсяв том, чтобы просто не позволять удалять записи в службе Каталог. Если в существующей системе удаление товарной позиции было сродни обеспечению недоступности товара для продажи или чему-то подобному, то можно реализовать мягкое удаления. Это можно сделать, например,с помощью столбца статуса, помечая данную строку как недоступную. В такойситуации запись об альбоме по-прежнему может запрашиваться службой Финансы.

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

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

Транзакции

Возможность вносить изменения в БД одной транзакцией намного облегчит разработку и сопровождение. Мы полагаемся на БД в том, что она обеспечивает безопасность и согласованность данных, оставляя нам беспокойство о другихвещах. Но при разделении данных по разным БД теряем выгоду от использования транзакции применительно к изменениям.

В типичной ситуации, когда упоминают транзакции в БД, обычно говорято транзакциях ACID. Давайте кратко рассмотрим ACID. ACID это аббревиатура, обозначающая ключевые свойстватранзакций в БД.

  • Атомарность (atomicity) - обеспечивает, чтобы все операции, завершаемые в рамкахтранзакции, либо все завершались успешно, либо все завершались безуспешно.

  • Согласованность (consistency) - при внесении изменений в БД обеспечивает,чтобы она оставалась в допустимом, согласованном состоянии.

  • Изоляция (isolation) - позволяет нескольким транзакциям работать одновременно без какого-либо вмешательства.

  • Долговечность (durability) - обеспечивает, чтобы после завершения транзакции мы былиуверены, что данные не будут потеряны в случае системного сбоя.

Мы по-прежнему можем использовать транзакциив стиле ACID, когда разбиваем БД, но объем этих транзакций уменьшается, как и их полезность. Предположим, мы отслеживаем процесс, связанный с подключением нового клиента к системе. В конце процесса предусматривается изменение статуса клиента с pending на verified. После завершения регистрации,мы хотим удалить соответствующую строку из таблицы Ожидающие регистрации. С одной БД это делается в рамках одной транзакции ACID либо обе новые строки записываются, либо ни одна из них не записывается. Однако если данные находятся в различных БД, то существует две транзакции, каждая из которых может сработать либо не сработать независимо от другой.

Рис. 6 Изменения в рамках двух разных транзакций.Рис. 6 Изменения в рамках двух разных транзакций.

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

Тогда что еще можно сделать? Первый вариант вообще не разбивать данные. Если у вас есть фрагменты состояния, которыми хотите управлять по-настоящему атомарным и согласованным способом, и не можете решить вопрос, как разумно получить этихарактеристики без транзакции в стиле ACID, то оставьте это состояние в однойБД и сохраните функциональность, которая управляет этим состояниемв одной службе (или в монолите)

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

Саги

Cага (saga) по своему дизайну представляетсобой алгоритм, который координирует многочисленные изменения в состоянии,но позволяет избегать необходимости запирания ресурсов на замок в течениедлительного периода времени. Мы делаем это путем моделирования шагов, привлекаемых в качестве дискретных действий, которые исполняются независимо.

Cага не дает нам атомарности в терминах АСID, к которой мы привыкли с обычной транзакцией базы данных. Поскольку мы разбиваем бизнес-процесс на отдельные транзакции, у нас нет атомарности на уровне самой саги. У нас есть атомарность для каждой "под-транзакции", поскольку каждая из них может относиться к АСID-транзакционному изменению, если это необходимо. Но вот что сага нам дает, так это достаточно информации для выяснения состояния, в котором она находится.

На рис. 7 изображен простой поток исполнения заказа. Здесь процесс исполнения заказа представлен как одна сага, причем каждый шагв этом потоке представляет собой операцию, которая выполняется другой службой. В каждой службе любое изменение состояния регулируется в рамках локальнойACID-транзакции.

Рис. 7 - Пример потока исполнения заказа наряду со службами,ответственными за проведение операции.Рис. 7 - Пример потока исполнения заказа наряду со службами,ответственными за проведение операции.

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

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

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

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

Рис. 8 Запуск отката всей саги.Рис. 8 Запуск отката всей саги.

Реализация саг

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

Cаги на основе оркестрации (orchestrated saga) используют центрального координатора(оркестровщик) для определения порядка исполнения и активации любого необходимого компенсирующего действия. Пример на рис. 9. Здесь Обработчик заказов, играя роль оркестровщика, координирует процесс исполнения. Он знает, какие службы необходимы для выполнения операции, и решает, когда следует вызывать эти службы.

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

Рис. 9 - Пример того, как оркестрированная сага используетсядля реализации процесса исполнения заказа.Рис. 9 - Пример того, как оркестрированная сага используетсядля реализации процесса исполнения заказа.

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

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

Саги на основе хореографии (choreographed saga) распределяют ответственность за функционирование саги среди нескольких сотрудничающих служб. Как мы увидимна рис. 10, саги на основе хореографии интенсивно используютсобытия для сотрудничества между службами.

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

Рис. 10 - Пример саги на основе хореографии для реализации исполнения заказа.Рис. 10 - Пример саги на основе хореографии для реализации исполнения заказа.

В такой архитектуре службы друг о друге не знают. Им нужно только знать, что делать, когда получено определенное событие. Это приводит к гораздо менее сопряженной архитектуре.

Оборотная сторона всего этого заключается в том, что теперь сложнее выяснить,что происходит. С этой архитектурой непонятно как выстроить модель того, каким предположительно должен быть процесс?Нужно смотреть на поведение каждой службы в изоляции и воссоздавать эту картину в своей голове.

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

Вывод

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

Ссылки на все части

Подробнее..

Strategy Design Pattern

13.04.2021 22:15:19 | Автор: admin

Всем привет, друзья. Меня зовут Alex, я профессиональный разработчик и создатель программных продуктов в веб индустрии. Много лет изучаю языки, делюсь опытом с другими.

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

В чем суть?

Design patter Strategy или шаблон проектирования Стратегия относится к поведенческим шаблонам проектирования. Его задача - выделить схожие алгоритмы, решающие конкретную задачу. Реализация алгоритмов выносится в отдельные классы и предоставляется возможность выбирать алгоритмы во время выполнения программы.

Шаблон дает возможность в процессе выполнения выбрать стратегию (алгоритм, инструмент, подход) решения задачи.

В чем проблема?

Рассмотрим задачи, при решении которых можно применять такой подход.

Представьте, что перед вами стоит задача написать веб-портал по поиску недвижимости. MVP (Minimum ViableProduct) или минимально работающий продукт был спроектирован и приоритизирован вашей командой Product Managerов и на портале должен появиться функционал для покупателей квартир. То есть целевые пользователи вашего продукта в первую очередь - это те, кто ищет себе новое жилье для покупки. Одной из самых востребованных функций должна быть возможность:

  • Выбрать область на карте, где покупатель желает приобрести жилье

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

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

Но тут приходят к вам Product Manager'ы и говорят, что нужно добавить возможность искать и отображать недвижимость, которая сдается в аренду. У нас появляется еще один тип пользователя - арендаторы. Для арендаторов не так важно показывать фильтры по цене, им важно состояние квартиры, поэтому нужно отображать фотографии арендуемых квартир.

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

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

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

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

  • Основной алгоритм поиска квартир был реализован в одном супер-классе

  • Алгоритм выбора и отображения элементов интерфейса был реализован в одном супер-классе

  • Изменения в этих классах, сделанные разными программистами, приводили к конфликтам и необходимости регрессивного тестирования

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

  • Больше времени стало уходить на разработку, тестирование, появилось множество багов.

Супер-класс с единым методом реализации алгоритма.Супер-класс с единым методом реализации алгоритма.

Какое решение?

В данном примере мы имеем несколько алгоритмов для одной функции:

  • Поиск квартир с продажей

  • Поиск квартир в аренду

  • Отображение или нет различных наборов фильтров

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

Шаблон проектирования Стратегия - решает такую задачу. Онпредлагает выделить семейство похожих алгоритмов, вынести их в отдельные классы. Это позволит без проблем изменять нужный алгоритм, расширять его, сводя к минимум конфликты разработки, зависимости от других классов и функционала. Вместо того, чтобы реализовывать алгоритм в едином классе, наш класс будет работать с объектами классов-стратегиями через объект-контекста и в нужным момент делегировать работу нужному объекту. Для смены алгоритма достаточно в нужным момент подставить в контекст нужный объект-стратегию.

Чтобы работа нашего класса была одинаковой для разного поведения, у объектов-стратегии должен быть общий интерфейс. Используя такой интерфейс вы делаете независимым наш класс-контекста от классов-стратегий.

Диаграмма классов шаблона StrategyДиаграмма классов шаблона Strategy

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

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

Несмотря на то, что каждый класс-стратегия реализует поиск по своему алгоритму, с применением необходимых фильтров (продажа, аренда, загородный дом, жилая, не жилая и т.д.), не исключено, кстати, что они будут работать с разными базами - все это для веб-интерфейса, который отображает список найденных объектов, не имеет никакого значения. После того, как пользователь выбрал интересующий его тип недвижимости в фильтрах на сайте, будет происходить запрос в контроллер на backend, с экшеном получения данных по входящим фильтрам и типам пользователя.

Задача контроллера определить класс-стратегию и запросить у класса-контекста данные для отображения, передав ему известный набор фильтров. Класс-контекст в этой схеме - это класс, которые реализует метод поиска квартир по заданным фильтрам. На диаграмме классов выше мы видим, что класс контекста определяет метод getData, и принимает аргументы filters. У него должен быть конструктор, принимающий активный в данный момент объект-стратегии и сеттерsetStrategy, устанавливающий активную стратегию. Такой метод пригодится для случая, когда пользователь меняет тип искомого объекта, например, он ищет недвижимость на продажу и хочет снять квартиру.

Пример реализации

Ниже рассмотрим пример, как решается описанная задача на языке GOlang. Первое что сделаем - определим интерфейс с методом doSearch:

Strategy.goStrategy.go

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

Для реализации конкретных алгоритмов создаем два файла. В каждом файле определяется свой определяемый тип с базовым типом struct, реализующие интерфейс Strategy. Соответственно, в методы, определяемые интерфейсом для каждого алгоритма, будут передаваться пользовательские фильтры. Реализации выглядит следующим образом:

FirstAlgorithm.goFirstAlgorithm.goSecondAlgorithm.goSecondAlgorithm.go

Посмотрим на нашу диаграмму классов. Нам осталось реализовать класс-контекста и клиентский код вызова конкретных алгоритмов в нужным момент. Как это сделать? Для создания слоя класса-контекста реализуем исходник, реализующий:

  • определяемый тип в базовым типом struct

  • функцию initStrategy, инициализирующий стратегию по-умолчанию и пользовательские фильтры

  • метод типа struct setStrategy, устанавливающий активную стратегию

  • и функция getData, вызывающий конкретную стратегию и возвращаемый данные для показа пользователю.

Context.goContext.go

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

Client.goClient.go

Вот вывод такого подхода:

First implements strategy map[role:1]

Second implements strategy map[role:2]

Как видите, мы можем управлять вызовом разных алгоритмов в зависимости от контекста и пользовательских фильтров. Алгоритмы могут создавать ветвления в зависимости от входных фильтров и других параметров, переданных из клиентского кода в методы, реализующие конкретные алгоритмы. Вот такой интересный пример.

Объектно-ориентированный подход можно посмотреть. например, в этом курсе. Там показан пример на PHP.

Когда применять?

Напоследок поговорим когда применяется шаблон Strategy?

  1. У вас есть множество похожих реализаций отличающихся незначительным поведением. Можно вынести отличающее поведение в классы-стратегии, а повторяющий код свести к единому классу-контекста.

  2. Ваш алгоритм реализован в супер-классе с множественными условными операторами. Выделите блоки условных операторов в отдельные классы-стратегии, а управление вызовов нужных доверьте классу-контекста.

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

  4. В зависимости от ситуации вы можете менять стратегию выполнения задачи в процессе выполнения программы. Например, в зависимости от скорости интернета использовать разные стратегии-поведения, возвращающие разный набор данных для отображения страницы.

Подведем итог

Друзья, мы познакомились с поведенческим шаблоном проектирования Strategy. Шаблон используется для выделения схожих алгоритмов, решающих конкретную задачу. Посмотрели с вами реализацию на языке GOlang, ознакомились в возможностями подхода и разобрали когда его лучше применять.

Рад был с вами пообщаться, Alex Versus. Успехов!

Подробнее..

Перевод Как создать архитектуру для работы с высокой нагрузкой вашего веб-проекта?

26.05.2021 16:17:30 | Автор: admin

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

Помните "Черные Пятницы", которые так популярны среди людей? Знаете ли вы, что иногда сайты и веб-приложения не выдерживают такого огромного наплыва пользователей и в результате этого нередко теряют много денег?

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

Медленная загрузка страниц и не полностью загружающийся контент, сбои, случайные ошибки, обрыв связи с Интернетом - это возможные последствия отсутствия архитектуры для работы с высокой нагрузкой. Вот почему создание программной системы, способной справляться с высокими нагрузками, имеет большое значение. Даже если ваш веб-проект совсем небольшой, в какой-то момент может возникнуть наплыв пользователей или возникнет необходимость в эластичном масштабировании.

Ознакомьтесь с некоторыми фактами о высокой нагрузке:

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

  • Если один экземпляр (instance) одновременно обслуживает 10 000 соединений - это высокая нагрузка. Высокая нагрузка - это одновременное обслуживание тысяч и миллионов пользователей

  • Если вы развертываете веб-решение на AWS (Amazon Web Services), Microsoft Azure или Google Cloud Platform, вы поддерживаете архитектуру для работы с высокой нагрузкой.

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

  • Медленная или бесконечная загрузка страниц

  • Случайные ошибки

  • Разрывы соединений с веб-сервером

  • Неполная загрузка контента

  • Снижение активности пользовательской аудитории

  • Потери клиентов

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

Принципы построения высокоэффективных решений

Динамика и гибкость

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

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

При разработке крупномасштабных веб-решений уделяйте особое внимание гибкости, так как это позволит вам легко вносить изменения и расширения. Гибкость, отсутствие предварительного планирования всех аспектов - важнейшая характеристика любой быстрорастущей программной системы.

Постепенный рост проекта

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

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

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

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

Масштабирование веб-решения - это постепенный процесс, состоящий из 4 основных этапов:

  • Анализ нагрузки

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

  • Передача этих областей отдельным узлам и их оптимизация

  • Анализ нагрузки

Разработка масштабируемой архитектуры веб-проекта

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

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

Разделение базы данных

Чаще всего первым узлом, который подвергается нагрузке, является база данных. Каждый запрос от пользователя к приложению - это, как правило, от 10 до 100 запросов к базе данных. Вынесение базы данных на отдельный сервер повысит ее производительность и снизит негативное воздействие на другие компоненты (PHP, Nginx и т.д.).

Миграция базы данных

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

  • Используйте простое решение - разместите объявление о планируемых работах на сайте и сделайте перенос. Лучше сделать это ночью, когда активность пользовательской аудитории минимальна.

  • Используйте репликацию для синхронизации данных с одного сервера на другой. После настройки необходимо изменить IP-адрес базы данных в приложении на новый сервер. Затем - выключить старый сервер.

Разделение веб-сервера

Далее следует отделить веб-сервер, выделение которого на отдельный узел позволит оставить больше ресурсов для приложения. Если говорить о примере с PHP, то необходимо настроить развертывание продукта как на сервер Nginx, так и на сервер PHP, представляющий бэкенд.

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

server {server_name ruhighload.com;root /var/www/ruhighload;index index.php;location ~* .(php)$ {fastcgi_pass 10.10.10.1:9000;fastcgi_index index.php;include fastcgi_params;fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;}}

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

Используйте несколько бэкендов

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

При инсталляции бэкендов убедитесь, что они имеют одинаковую конфигурацию. Используйте Nginx для балансировки нагрузки между ними. Для этого следует определить список бэкендов в апстрим (upstream) и использовать его в конфигурации:

upstream backend {server 10.10.10.1;server 10.10.10.2;server 10.10.10.3;}server {server_name ruhighload.com;root / var / www / ruhighload;index index.php;location ~ * . (php) $ {fastcgi_pass backend;fastcgi_index index.php;include fastcgi_params;fastcgi_param SCRIPT_FILENAME $ document_root $ fastcgi_script_name;}}

Когда вы начнете использовать несколько бэкендов, запросы от одного и того же пользователя будут отправляться на разные серверы. Это потребует единого репозитория для всех сессий.

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

Очереди задач и балансировка DNS

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

DNS поддерживает балансировку по принципу Round Robin, позволяя указать несколько IP-адресов принимающих веб-серверов, называемых фронтендами. В данном случае необходимо инсталлировать несколько одинаковых фронтендов, чтобы DNS выдавал разные IP-адреса разным клиентам. Таким образом, вы обеспечите балансировку между фронтендами.

Файловые хранилища

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

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

Сделать это можно следующим образом:

  • Определить отдельный поддомен для файлового сервера.

  • Развернуть на сервере Nginx и небольшое приложение, которое может хранить файлы и обрабатывать их

  • Масштабирование путем добавления новых серверов и поддоменов (например, images1, images2, images3 и т.д.)

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

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

Однако ключевым моментом является экономическая эффективность. Скажем, у вас 100 тысяч пользователей и один сервер. Значит, чтобы получить 130 тысяч из них, нужно разместить еще один сервер. Кажется, что это довольно сложно, не так ли?

Поэтому следует сделать один шаг назад и подумать - какая часть системы вызывает проблему? Если это база данных, то перед началом разработки проекта выберите высокомасштабируемую базу данных. Или вы можете использовать несколько баз данных, например, одну для записи и одну для чтения (CQRS).

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


Перевод материала подготовлен в рамках запуска курса "Highload Architect".

Всех желающих приглашаем на бесплатный демоурок Выбор архитектурного стиля (микросервисы, СОА и монолиты).

- ЗАПИСАТЬСЯ НА ДЕМОУРОК

Подробнее..

Подпишись, чтобы не пропустить События

16.06.2021 20:20:54 | Автор: admin

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

В примерах в статье про автоматы я использовал следующую конструкцию:

Game.Event.Invoke("joystick_updated", input);

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

public static class Game{    public static FSM Fsm = new FSM();    public static EventManager Event = new EventManager();       public static ObservableData Data = new ObservableData();...

В этих примерах можно увидеть некоторые вольности в деталях реализации. При масштабировании проекта, например, придется отказаться от статического контекста и на основе класса Game реализовать компоненты, назовем их претенциозно MonoBehaviourPro с подобной структурой для сложных подсистем, и передавать ее в качестве контекста автомату и компонентам этих подсистем. Я намеренно сглаживаю эти углы для большей наглядности примера. Сегодня мы рассмотрим класс с многострадальным названием EventManager, так как он является зависимостью ObservableData и без него мы не сможем двинуться дальше. По ссылке можно увидеть полную реализацию класса EventManager, принцип его работы предельно прост. Мы храним список делегатов c произвольной сигнатурой, подписанных на события со строковым ключом.

Важно, что мы работаем с Generic-структурой, поэтому следует помнить о Type safety. Тип аргумента при отправке события должен соответствовать сигнатурам функций, подписанных на него. Также, можно заметить, что EventManager отдельно хранит binds и binds_global и имеет отдельный интерфейс для работы с ними. Это реализация, специфичная для Unity. Дело в том, что там существует система сцен, позволяющая подгружать или выгружать сцены и объекты. И разница между этими двумя словарями в том, что первый очищается при выгрузке сцены. В идеальном мире мы всегда подписываем объект в Awake и отписываем его в OnDestroy. В таком случае можно было бы обойтись одним binds, не очищая его никогда. Каждый объект подписывается и отписывается в рамках своего жизненного цикла и разве что при переходе между сценами происходило бы немного лишней работы над поштучным отписыванием выгружаемых объектов. Но такой подход не прощает ошибок, выгруженный подписчик в лучшем случае сразу сломает вызов делегата и будет найдена, а в худшем - станет причиной утечки памяти. Так что, в качестве "защиты от дурака" лучше при переходе явно отписывать все, что не было обозначено как Global.

Итак, интерфейс EventManager cводится к 5 методам:

        public void Bind<T>(string name, Action<T> ev)        public void BindGlobal<T>(string name, Action<T> ev)        public void Unbind<T>(string name, Action<T> ev)        public void UnbindGlobal<T>(string name, Action<T> ev)                  public void Bind(string name, Action ev)        public void BindGlobal(string name, Action ev)        public void Unbind(string name, Action ev)        public void UnbindGlobal(string name, Action ev)                  public void Invoke<T>(string name, T arg)                  public void Invoke(string name)

Мы можем подписываться на события и отправлять их. И все это с аргументом произвольного типа. В примере из статьи про FSM мы передавали ввод с джойстика в автомат и, если состояние предусматривает такую возможность, передавали в EventManager событие изменения положения джойстика , на которое может подписаться компонент, управляющий положением игрока(Или потомок MonoBehaviourPro, какой нибудь PlayerController, который передаст информацию о вводе в свой автомат, и если игрок в состоянии SPlayerDriving , будет передавать ввод с джойстика уже автомобилю, за рулем которого он сидит, а если в SPlayerClimbing, джойстик будет двигать игрока перпендикулярно нормали плоскости, по которой он движется, с соответствующей анимацией. Но это уже более сложные примеры, не будем на этом задерживаться). Или же, на входе в состояние игры SWin мы можем отправить событие level_done, а на него подписать анимацию экрана победы, конфетти, и чего там еще ваш ГД придумает.

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

Таким образом, автомат в процессе своей работы может отправлять события подписавшимся на них объектам через EventManager. В следующей статье я расскажу о возможностях, предоставляемых классом ObservableData в связке с описанными инструментами.

Эта статья - вторая в серии:
- Разделяй и властвуй Использование FSM в Unity
- Подпишись, чтобы не пропустить События

Подробнее..

Кластеризация и классификация больших Текстовых данных с помощью М.О. на Java. Статья 3 АрхитектураРезультаты

24.01.2021 14:08:22 | Автор: admin

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

Статья описывает архитектуру системы, алгоритма, а также визуальные результаты. Все детали теории и алгоритмов вы найдете в первых двух статьей.

Архитектуры системы можно разделить на две основные части: веб приложение и программное обеспечение кластеризации и классификации данных

Алгоритм программного обеспечение для машинного обучение состоит из 3 основных частей:

  1. обработка естественного языка;

    1. токенизация;

    2. лемматизация;

    3. стоп-листинг;

    4. частота слов;

  2. методы кластеризации ;

    1. TF-IDF ;

    2. SVD;

    3. нахождение кластерных групп;

  3. методы классификации Aylien API.

Обработка естественного языка

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

Ниже приводим сравнение при запуске алгоритмов Лемматизации и Стеммитизации:

Общее количество слов: 4173415Количество слов после приминение Лемматизации: 88547Количество слов после приминение Стеммитизации: 82294

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

characterize, design, space, render, robot, face, alisa, kalegina, university, washington, seattle, washington, grace, schroeder, university, washington, seattle, washington, aidan, allchin, lakeside, also, il, school, seattle, washington, keara, berlin, macalester, college, saint, paul, minnesota, kearaberlingmailcom, maya, cakmak, university, washington, seattle, washington, abstract, face, critical, establish, agency, social, robot, building, expressive, mechanical, face, costly, difficult, robot, build, year, face, ren, der, screen, great, flexibility, robot, face, open, design, space, tablish, robot, character, perceive, property, despite, prevalence, robot, render, face, systematic, exploration, design, space, work, aim, fill, gap, conduct, survey, identify, robot, render, face, code, term, property, statistics

а стеммитизация обрезает окончание и в некоторых случаях удаляет нужные буквы, теряя основной смысл слово:

character, design, space, render, robot, face, alisa, kalegina, univers, washington, seattl, washington, grace, schroeder, univers, washington, seattl, washington, grsuwedu, aidan, allchin, lakesid, also, il, school, seattl, washington, keara, berlin, macalest, colleg, saint, paul, minnesota, kearaberlingmailcom, maya, cakmak, univers, washington, seattl, washington, abstract, face, critic, establish, agenc, social, robot, build, express, mechan, face, cost, difficult, mani, robot, built, year, face, ren, dere, screen, great, flexibl, robot, face, open, design, space, tablish, robot, charact, perceiv, properti, despit, preval, robot, render, face, systemat, explor, design, space, work, aim, fill, gap, conduct, survey, identifi, robot, render, face, code, term, properti, statist, common, pattern, observ, data, set, face, conduct, survey, understand, peopl, percep, tion, render, robot, face, identifi, impact, differ, face, featur, survey, result, indic, prefer, vari, level, realism, detail, robot, facecharacter, design, space, render, robot, face, alisa, kalegina, univers, washington, seattl, washington, grace, schroeder, univers, washington, seattl, washington, grsuwedu, aidan, allchin, lakesid, also, il, school, seattl, washington, keara, berlin, macalest, colleg, saint, paul, minnesota, kearaberlingmailcom, maya, cakmak, univers, washington, seattl, washington, abstract, face, critic, establish, agenc, social, robot, build, express, mechan, face, cost, difficult, mani, robot, built, year, face, ren, dere, screen, great, flexibl, robot, face, open, design, space, tablish, robot, charact, perceiv, properti, despit, preval, robot, render, face, systemat, explor, design, space, work, aim, fill, gap, conduct, survey, identifi, robot, render, face, code, term, properti, statist, common, pattern, observ, data, set, face, conduct, survey, understand, peopl, percep, tion, render, robot, face, identifi, impact, differ, face, featur, survey, result, indic, prefer, vari, level, realism, detail, robot, face

Методы кластеризации

Для применения алгоритма tf-idf нужно подсчитать сколько раз слово встречается в каждом документе. Можно использовать HashMap, где ключ - слово, значение - кол-во.

После этого нужно построит матрицу документы-слова:

Далее по формуле вычисляем tf-idf:

Следующий этап, использование метода сингулярного разложение, где на вход приходит результат tf-idf. Пример выходных данных алгоритма сингулярного разложение:

-0.0031139399383999997 0.023330604746 -1.3650204652799997E-4-0.038380206566 0.00104373247064 0.056140327901-0.006980774822399999 0.073057418689 -0.0035209342337999996-0.0047152503238 0.0017397257449 0.024816828582999998-0.005195951771999999 0.03189764447 -5.9991080912E-4-0.008568593700999999 0.114337675179 -0.0088221197958-0.00337365927 0.022604474721999997 -1.1457816390099999E-4-0.03938283525 -0.0012682796482399999 0.0023486548592-0.034341362795999995 -0.00111758118864 0.0036010404917-0.0039026609385999994 0.0016699372352999998 0.021206653766000002-0.0079418490394 0.003116062838 0.072380311755-0.007021828444599999 0.0036496566028 0.07869801528199999-0.0030219410092 0.018637386319 0.00102082843809-0.0042041069026 0.023621439238999998 0.0022947637053-0.0061050946438 0.00114796066823 0.018477825284-0.0065708646563999995 0.0022944737838999996 0.035902813761-0.037790461814 -0.0015372596281999999 0.008878823611899999-0.13264545848599998 -0.0144908102251 -0.033606397957999995-0.016229093174 1.41831464625E-4 0.005181988760999999-0.024075296507999996 -8.708131965899999E-4 0.0034344653516999997

Матрицу SVD можно использовать как координаты в трехмерном пространстве.

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

Теперь нужно применить данную операцию и для терминов, то есть слов.

Последний этап метода кластеризации найти кластерные группы. Так как у нас уже есть трехмерная пространство, где хранятся точки документов и терминов в виде вершин, то нужно соединить эти документы и слова использовав схожий метод кластеризации DBSCAN. Для определения расстояние между документом и словом используется Евклидовое расстояние. А радиус можно определить по формуле ниже. В данном примере и при тестировании используется r=0.007. Так как в пространстве находится 562 документов и более 80.000 тысяч слов, то они расположены близко. При большом радиусе алгоритм будет связывать термин и документ в один кластер, которые не должны быть в одной группе.

r=max(D)/n

где max(D) это дистанция между документом и самой дальней точкой термина, то есть максимальная дистанция документа в пространстве. n - это количество документов в пространстве

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

После этого нужно всего лишь соединить вершины документов, которые имеют общие вершины терминов. Для соединения документов нужно чтобы общее число терминов было больше 4-х. Формула определение общего сила слов (в данном случае > nt)

nt=N/S

N это количество кластерных групп термин - документов, S это количество связей в семантическом пространстве.

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

Методы классификации Aylien API

Для классификации в инструменте Aylien API всего лишь нужно передать любой текст. API вернет ответ в виде json объекта, где внутри есть категории классификации. Можно было бы отправлять весь текст каждого документа в одной группе кластеров через API и получить категории классификации. Для примера рассмотрим 9 групп кластеров, которые состоят из статьи про ИТ технологии. Все тексты документов каждой группы записываются в массив и отправляют запрос POST через API:

String queryText = "select  DocText from documents where clusters = '" + cluster + "'";   OResultSet resultSet = database.query(queryText);   while (resultSet.hasNext()) {   OResult result = resultSet.next();   String textDoc = result.toString().replaceAll("[\\<||\\>||\\{||\\}]", "").replaceAll("doctext:", "")   .toLowerCase();   keywords.add(textDoc.replaceAll("\\n", ""));   }   ClassifyByTaxonomyParams.Builder classifyByTaxonomybuilder    = ClassifyByTaxonomyParams.newBuilder();   classifyByTaxonomybuilder.setText(keywords.toString());   classifyByTaxonomybuilder.setTaxonomy(ClassifyByTaxonomyParams.StandardTaxonomy.IAB_QAG);   TaxonomyClassifications response = client.classifyByTaxonomy(classifyByTaxonomybuilder.build());   for (TaxonomyCategory c : response.getCategories()) {   clusterUpdate.add(c.getLabel());   }

После успешного получение ответа от сервиса методам GET, данные группы обновляются:

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

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

Разработка веб-интерфейса

Цель разработки веб-интерфейса наглядный вид результата использование алгоритма кластеризации и классификации. Это дает пользователю удобный интерфейс не только увидеть сам результат, но и в дальнейшем использовать эти данные для нужд. Так же разработка веб-интерфейса показывает, что данный метод можно успешно использовать для онлайн библиотек. Веб приложение было написано с использованием Фреймворка Vaadin Flow:

В данном приложении есть следующие функции:

  • Документы, разделенные по предметам методом кластеризации и классификации.

  • Поиск по ключевым словам.

  • Поиск по хэш-тегам.

  • Весь список документов в базе данных, где есть возможность поиска по ИД документа, наименованию документа, предметам кластеров, ключевым словам и по хэш-тэгам.

  • Возможность скачивание файла.

Список документов классификации по предмету Technology & Computing:

Список документов найденные по ключевым словам:

Табличный список всех документов:

Заключение

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

Разработка алгоритма кластеризации, который включают в себе последовательное применение алгоритмов лемматизации, токенизации, стоп-листниг, tf-idf, сингулярного разложение. Первые три метода относится к методу обработки естественного языка, данные методы можно изменить под язык обрабатываемого текста. Для нахождение кластерных групп используется алгоритм на основе метода DBSCAN и использование Евклидового расстояние для определения расстояние между объектами. При исследовании было доказано что точность кластеризации зависит от отношения количества кластеров к количеству объектов в одном кластере. Количество кластеров определяется радиусом каждого документа, а количество объектов в одном кластере определяется средним количеством общих объектов, в данном случае слов или терминов. Алгоритм кластеризации описанный в работе можно использовать не только для классификации групп, а и для других целей, таких как нахождение ассоциативных правил, нахождение групп документов, которые схожи по смысловому тексту и т.д.

В результате исследование, было предложено использование NoSQL базы данных, о именно OrinetDB, который поддерживает все 4 модели NoSQL. Данный тип базы данных очень хорошо подходит для хранения результатов алгоритма кластеризации, так как данный результат является не реляционным. Стоит отметить что OrientDB очень удобен для хранения, обработки и визуализации хранимых данных.

Для классификации кластерных используется Aylien API, который использует подход классификации по таксономии и на базе кодов. В результате исследовании кластерные группы были разделены по предметным областям, который включает в себя более 100 контентной категории. Данный метод можно заменить и другими, более специфическими алгоритмами машинного обучение, таких как метод опорных векторов, метод k-ближайших, нейронную сеть. Но так как данные методы требуют большое количество данных который используется для построения модели, в данной работе было использована готовая модель классификации.

Подробнее..

Перевод Инженерная надежность и отказоустойчивость распределенной системы

08.06.2021 16:18:29 | Автор: admin

Это гостевая публикация отПэдди Байерса (Paddy Byers), сооснователя и технического директораAbly платформы для стриминга данных в реальном времени. Оригинал статьи опубликован вблоге Ably.

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

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

Для начала дадим несколько определений:

Надежность cтепень того, насколько пользователи могут положиться на продукт или сервис для решения своих задач.Доступностьиустойчивостьявляются видами надежности.
Доступность степеньготовностипродукта или сервиса к эксплуатации по требованию. Это понятие часто сcодят к обеспечению необходимого излишка ресурсов с учетомстатистически независимых отказов.
Устойчивость cпособность продукта или сервисасоответствовать заявленным характеристикамв процессе использования. Это значит, что система не просто готова к эксплуатации: благодаря дополнительным мощностям, предусмотренным в ходе проектирования,она может продолжать работать под нагрузкой, как и ожидают пользователи.
Отказоустойчивость способность системы сохранятьнадежность(доступность и устойчивость) при отказе отдельных компонентов или сбоях в подсистемах.

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

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

Если сервис не способен безотказно работать по требованию, значит, ему не хватает доступности. А если он готов к работе, но при этом отклоняется от заявленных характеристик в процессе использования, значит, не хватает устойчивости. Методы проектирования отказоустойчивых систем направлены на устранение этих недостатков и обеспечение бесперебойной работы системы как для бизнеса, так и для отдельных пользователей.

Доступность, устойчивость и состояние компонентов системы

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

В физическом мире традиционно различают:

  • ситуации, угрожающие доступности, когда можно остановить и затем возобновить работу сервиса например, остановить автомобиль, чтобы сменить покрышку;

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

Разные требования к обеспечению бесперебойной работы определяют способ удовлетворения потребности в дополнительных мощностях.

В контексте распределенных систем, таких как Ably, существует аналогичное различие между компонентами, работающимис сохранением состоянияибез сохранения состояния.

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

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

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

Отказы неизбежны и естественны

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

В отличие от физического мира, отказы в цифровых системах обычно не определяются двумя состояниями. Обычные показатели устойчивости компонентов (например, среднее время безотказной работы) к таким системам не применимы. Сервисы выходят из строя постепенно, после нескольких сбоев. Классический пример здесь задача византийских генералов.

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

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

Сервисы без сохранения состояния

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

Таким объектам достаточно иметь несколько независимо доступных компонентов, чтобы сервис продолжал работать. Без привязки к состоянию устойчивость отдельных составляющих не имеет значения.

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

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

  • Как сохранить работоспособность системы после разных типов отказов?

  • Какой уровень избыточности возможно обеспечить?

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

  • Каких эксплуатационных расходов требует управление этим уровнем избыточности?

Выбранное решение должно удовлетворять следующим критериям:

  • Требования клиентов к высокой доступности сервиса

  • Уровень эксплуатационных расходов для бизнеса

  • Инженерная целесообразность

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

Чтобы увеличить степень статистической независимости отказов в Ably,мы распределили мощности по множественным зонам доступности в разных регионах. Оказывать услуги из нескольких зон доступности в одном регионе проще AWS делает это практически без усилий. Зоны доступности в целом хорошо позволяют поддерживать независимость отказов, но их создание само по себе предполагает обеспечение необходимой избыточности для поддержания высокого уровня доступности.

Однако это еще не все. Мы не можем полностью положиться на один регион иногда несколько зон доступности отказывают одновременно или все они становятся недоступными из-за локальных проблем со связью. А иногда региональные ограничения мощности делают поддержку сервисов невозможной. Поэтому мы обеспечиваем доступность нашего сервиса, предоставляя его из множества регионов. Это лучший способ гарантировать статистическую независимость отказов.

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

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

Сервисы с сохранением состояния

В Ablyустойчивость определяется бесперебойной работойсервисов с сохранением состояния, и добиться этого намного сложнее, чем просто обеспечить доступность.

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

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

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

В Ably мы предоставляем достаточно резервных мощностей для ресурсов без сохранения состояния, чтобы удовлетворить требования клиентов к доступности. Однако объектам с сохранением состояния необходимы не только дополнительные ресурсы, но и четкие инструкции по их использованию, чтобы обеспечить гарантированную функциональную бесперебойность сервиса.

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

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

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

Когда клиент отправляет сообщение в Ably, сервис принимает его и уведомляет, была попытка публикации успешной или нет. В этом случае главный вопрос в контексте обеспечения доступности будет таким:

В течение какой доли времени сервис может принимать (и обрабатывать) сообщения, а не отклонять их?

Минимальный целевой показатель доступности для нас 99,99; 99,999 или даже 99,9999%.

Если вы попытались опубликовать сообщение и мы уведомили вас, что попытка не удалась, это говорит онехватке доступности. Это плохо, но, по крайней мере, вы в курсе ситуации.

А вот если мы ответили: Да, мы получили ваше сообщение, но не смогли выполнить последующую обработку, это уже другой тип отказа, связанный с невозможностью обеспечить гарантированный функционал сервиса. Этонехватка устойчивости, которая является для распределенных систем намного более серьезной проблемой. Ее решение требует масштабных и сложных инженерно-технических работ.

Архитектурный подход к обеспечению устойчивости

Следующие две иллюстрации наглядно демонстрируют архитектурный подход, который мы внедрили в Ably для оптимального использования избыточности при обработке сообщений.

Размещение роли с сохранением состояния

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

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

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

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

Это становится возможным благодаря последовательному алгоритму размещения в хеш-кольце

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

Выявить, хешировать, продолжить

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

Даже если роль не была выполнена и произошла потеря состояния, нужно обеспечить его поддержку (с необходимой избыточностью), чтобы восстановить непрерывное выполнение роли. Именно эта непрерывность позволяет сервису сохранять устойчивость к отказам такого рода. Состояние всех сообщений в очереди сохраняется во время перемещения роли. Если вместо этого у нас получилось просто переназначить роль, сохранив состояние, значит, мы обеспечили доступность сервиса но не егоустойчивость.

Слой обеспечения постоянства канала

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

Во-первых, данные о приеме сообщения записываютсякак минимум в двух разных зонах доступности. Во-вторых, к этим зонам доступности предъявляются те же требования избыточности при последующем процессе обработки данных. Это главный принцип работы слоя обеспечения постоянства в Ably: мы записываем сообщение в нескольких местах и следим за тем, чтобы процесс записи был транзакционным. Мы прекращаем его, только убедившись, что попытка записи быланеудачнойилиоднозначно успешной. Зная это, мы можем быть уверены, что дальнейшая обработка будет произведена даже при отказе ролей, ответственных за этот процесс.

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

Благодаря нашей математической модели при отказе узла мы точно знаем, сколько времени уйдет на выявление причины сбоя, достижение консенсуса и последующего перемещения роли. Эти данные вкупе с частотой отказов для каждой зоны доступности позволяют создать вероятностную модель возникновения комплексного сбоя, который может привести к потере текущего состояния сервиса. Описанные базовые принципы позволяют нам гарантироватьустойчивость работы сервиса с вероятностью 99,999999%.

Вопросы внедрения отказоустойчивости

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

Достижение консенсуса в глобально распределенных системах

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

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

Поэтому наши одноранговые узлы используютпротокол Gossip: он отказоустойчив, способен к работе в нескольких регионах и позволяет достичь согласования по прошествии некоторого времени. Gossip позволяет распространять между регионами данные о топологии сети и создает ее карту. Достигнутый таким образом консенсус об общем состоянии используется в качестве основы для распространения данных на уровне всего кластера.

Работоспособность не определяется двумя состояниями

Классическая теория, которая легла в основу разработки протоколов Paxos и Raft, базируется на том, что при отказе или проблемах с работоспособностью сущности не просто сбоят или перестают отвечать на запросы. Вместо этого они могут выдавать отдельные симптомы неполадок, такие как отклонение запросов, высокий процент ошибок или в целом нестабильное и некорректное поведение (см.Задача византийских генералов). Эта проблема стоит очень остро и касается даже взаимодействия клиентов с сервисом.

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

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

Проблема доступности ресурсов

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

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

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

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

Проблема масштабирования ресурсов

Добавим к сказанному выше, что проблема может возникнуть не только с доступностью ресурсов, но и вследствие частоты запросов на их масштабирование. В стабильной, работоспособной системе у вас может бытьNканалов,Nподключений,Nсообщений иNмощностей для их обработки. Теперь представим, что произошел отказ, который привел к сбою в работе кластера изNинстансов. Если объем работ, необходимых для компенсации сбоя, составляет N, то затраты на поддержку резервных мощностей примут и вовсе неподъемный объем, так что единственным возможным решением станет переключение на другой регион или кластер.

Тривиальные механизмы обеспечения отказоустойчивости могут демонстрировать сложность O(N) или даже хуже, и это необходимо учитывать при поиске решения проблемы. Это еще одно напоминание о том, что отказ может произойти не только в результате поломки его причиной может стать слишком большой масштаб, сложность инфраструктуры и другие факторы нерационального использования ресурсов.

Заключение

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

Говоря об инжиниринге надежности в физическом мире, мы, как правило, разделяем понятиядоступностииустойчивости. Существуют широко известные методы достижения этих состояний посредством обеспечения отказоустойчивости и избыточности. В Ably мы используем аналогичные определения компонентов системы, выделяя те, что работаютбез сохранения состоянияис сохранением состояния.

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

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

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

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

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


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

Подробнее..

Дата-центрическая архитектура волшебная пуля от интеграционных проблем

16.06.2021 18:22:08 | Автор: admin

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

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

А что, если найдется способ избавиться от всех интеграционных проблем разом? Такая "волшебная пуля" существует, и называется она - дата-центрическая архитектура. Ее основная идея состоит в том, чтобы сделать центральным элементом корпоративной ИТ-архитектуры не бизнес-приложения, а данные. Этот принцип изложен в Манифесте дата-центрической архитектуры.

Представьте, что в компании существует единое виртуальное хранилище данных, в котором каждый бизнес-объект или событие существует в единственном экземпляре. Для наглядности можно вообразить, что идея системы MDM (Master Data Management) доведена до логически полного воплощения, и именно MDM является хранилищем всех корпоративных данных; бизнес-приложения не имеют собственных СУБД и работают только с объектами данных из MDM. Преимущества такой архитектуры очевидны:

  • Раз и навсегда отменяется необходимость в интеграционных процедурах.

  • Снижаются затраты на хранение данных за счет избавления от множества копий каждого бизнес-объекта в разных системах.

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

  • Повышается качество данных за счет избавления от неполных, не актуальных представлений бизнес-объектов.

  • Упрощается процесс замены одних бизнес-приложений на другие, потому что все они работают с одними и теми же данными. Можно легко и быстро создавать микро-фронт-энды для решения частных бизнес-задач.

"Это какая-то фантастика", скажут некоторые. Нельзя же выбросить все существующие бизнес-приложения и начать с чистого листа, вывернув наизнанку всю корпоративную ИТ-архитектуру?

Нужен плавный процесс перехода, и его вполне можно обеспечить, если думать о корпоративной платформе управления данными не только как о физическом хранилище, содержащем материализованные представления всех бизнес-объектов, но и как о логической витрине данных, способной извлекать эти объекты из любых хранилищ, в том числе баз данных и сервисов унаследованных бизнес-приложений. Клиенту платформы должно быть безразлично, где находятся нужные ему данные - в одном из множества хранилищ, спрятанных внутри платформы, или в СУБД какого-либо бизнес-приложения. На практике, вероятнее всего, нужный объект данных будет собран платформой в момент выполнения запроса из нескольких источников.

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

Чем такой подход отличается от создания "обычного" корпоративного облака данных (corporate data cloud) или озера данных (data lake)? Прежде всего - методологией использования платформы, особым вниманием к структуре данных и некоторой функциональной спецификой. Если обычный data lake часто представляет собой коллекцию наборов данных, созданных кем-то для решения конкретных задач и заведомо содержащих копию уже существующей где-то информации, то для дата-центрической архитектуры принципиально соблюдение принципа "один объект в реальном мире - один объект данных". И никаких физических срезов, по крайней мере персистентных...

Управление структурой корпоративных данных - отдельный и очень важный вопрос, которому часто уделяется слишком мало внимания. Задача описания структуры всей информации, с которой работает предприятие, может показаться настолько сложной, что никто и не думает ее решать; вместо этого создается множество структур данных под конкретные бизнес-задачи, что влечет очевидные последствия. Мы утверждаем, что эта задача может и должна решаться; она успешно решается на практике в конкретных проектах, нужно лишь использовать подходящие для этого технологии. Одним из возможных вариантов является описание структуры корпоративной информации с помощью онтологий (см. спецификацию OWL консорциума W3C).

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

Когда вся корпоративная информация становится структурированной и логически связной, она приобретает свойства корпоративного графа знаний (corporate knowledge graph), который открывает предприятию новый уровень аналитических возможностей и позволяет гораздо эффективнее монетизировать накопленную информацию.

Какими функциональными свойствами должна обладать платформа, являющаяся ядром дата-центрической архитектуры? Она должна:

  • Поддерживать хранение состояния каждого объекта данных на любой момент времени. Объекты данных - не просто отражения текущего состояния объектов или событий реального мира, а "четырехмерные" описания всех состояний на протяжении всего времени жизни объекта.

  • Хранить непрерывную историю модели (структуры) данных. Структура данных столь же изменчива, как и сами данные. Платформа должна иметь возможность представлять объекты данных в соответствии с любой версией структуры. Структура должна формально описывать смысл каждого элемента данных.

  • Поддерживать множество API для работы с данными, включая REST, GraphQL, SPARQL.

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

  • Иметь развитые инструменты управления доступом к данным и защиты конфиденциальной информации.

  • Поддерживать инструменты прослеживания происхождения данных (data provenance), контроля их качества (data quality), описания степени доверия к данным.

Построение подобных платформ с использованием онтологических моделей открывает и другие возможности. В онтологической модели можно описать в машинно-читаемой и автоматически исполняемой форме не только структуру данных, но и алгоритмы их обработки - правила контроля целостности, арифметических вычислений, дополнения информации (см. спецификации SHACL и SHACL Advanced Functions). Это позволяет по-новому взглянуть и на принцип low code: если в единой корпоративной платформе управления данными хранятся не только данные и описание их структуры, но и машинно-читаемое описание алгоритмов обработки данных, то новые бизнес-приложения, ориентированные на использование таких описаний, станут еще гибче и смогут изменять свое поведение "на лету" без вмешательства не только в код, но и в настройки приложений.

Платформы, отвечающие таким требованиям, уже существуют и применяются и на российском, и на зарубежном рынке.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru