reactivity as a pattern for low models coupling

Что такое реактивное программирование? Не Rx. И даже не Excel. Это архитектурный паттерн, позволяющий абсолютно иначе писать код. В статье мы устаканим фундаментальные знания, утвердимся в том, что React.js всё же является реактивным, и подумаем о том, как и когда нужно, а когда не нужно применять паттерны реактивного программирования.

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

Вы уже могли видеть связанную статью «Что такое состояние», где я подробно разбираю этот вопрос. А сегодня поговорим о второй стороне управления состоянием и потоками данных.

Проблемы, которые реактивное программирование поможет вам решить:

  • связанность кода и его разбиение на модули;
  • ленивая подгрузка модулей;
  • автоматическая инвалидация кеша.

▍ Определение


Реактивное программирование — парадигма программирования, предполагающая передачу ответственности за инициализацию обработки информации источнику информации.

Это определение — описание того общего, что есть у Rx и MobX — таких разных, но безусловно реактивных библиотек, признанных всей индустрией. Да, они полностью отличаются внешним API, но подкапотные механизмы одни и те же — динамический список подписчиков и какое-то условие (триггер) его обхода. Это очень простая механика, и она встречается постоянно. В этом нет ничего сакрального, многие стандартные API платформы и библиотеки её реализуют. Иногда доступ к подписчикам явный — метод subscribe или effect. Иногда он скрыт — JSX тег разворачивается в React.createElement, который ведёт к подписке на внутренний стейт.

Да, React.js также использует паттерны реактивного программирования, это легко проверить. Есть ли у нас контроль над выполнением функции рендера? Нет, React определяет это. Вы могли бы попросить запланировать обновление, но оно будет запущено, когда React примет решение об этом. Он несёт ответственность за запуск вычислений.

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

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

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

▍ Пример


Здесь и ниже мы будем находиться в контексте JavaScript, но все идеи относятся к любому ЯП.

У нас есть аватарка пользователя в шапке и на странице профиля, при её обновлении с этой страницы нужно подставить новый урл в двух местах. Классический подход: в handlePictureUpdate мы пишем такой код: document.querySelector('.profile .ava').src = newSrc; document.querySelector('.header .ava').src = newSrc. Важно тут то, что функция handlePictureUpdate находится в коде модуля профайла, но почему-то ходит в модуль шапки — это и есть связанность. Такой кодстайл имеет свойство расти по своей сложности, его чтение может давать не те результаты, на которые рассчитываешь, — код шапки не содержит информации о связи с профайлом. Всё это ведёт к неочевидным багам — мы обновили шапку, поменяв её класс на .app-header, и querySelector в handlePictureUpdate теперь будет падать с ошибкой TypeError: Cannot set properties of undefined (setting 'src'). Причём ошибку после такого изменения скорее всего не выявили бы, потому что она в другом модуле. А в интеграционных тестах профайла не было бы проверки того, что происходит в шапке — классика.

Кто-то скажет, что дело в отсутствии БЭМа и предсказуемых селекторов. Кто-то возмутится неиспользованием общей константы с названием селектора шапки. Кто-то укажет на отсутствие проверки на undefined после querySelector — TypeScript бы подсказал! Такой маленький пример и уже так много проблем. Но это всё вопросы прикладного кода, которые в разных ситуациях будут разными. Возможно ли решить проблему подобного характера с архитектурной точки зрения, принципиально избавившись от необходимости перепроверять один модуль при рефакторинге другого?

▍ DDD и SSoT


Принцип Single Source of Truth (SSoT) означает, что в системе существует единственный актуальный и согласованный источник данных определённого типа, а все связанные модели работают с данными и процессами этого типа только через этот источник. Предметно-ориентированное проектирование (domain-driven design, DDD) говорит нам о том, что эти типы данных должны исходить от бизнес-сущностей — доменов: профиль пользователя, сформированный заказ товара. Модель — реализация домена в коде, страница пользователя и форма просмотра и создания, редактирования заказа.

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

Пример-предыстория о всё том же адресе аватарки. Когда приложение только начинали делать, выделенной страницы пользователя не было, были только основные формы для реализации бизнеса: список товаров, оформление заказа. Со временем решили повысить UX пользователя, сделать интерфейс дружественным и домашним и решили отображать на нём всегда что-то очень знакомое для пользователя — его аватарку. При клике по квадратной заглушке в правой части шапки можно было выбрать картинку и загрузить её. Логика этой загрузки лежала, соответственно, в шапке. Ещё позже решили сделать страницу редактирования профиля, чтобы адрес можно было заранее сохранить и редактировать, ну и картинку покрупнее посмотреть, заменить и покропить. У программиста встала дилемма — двигать код загрузки из файла с шапкой в файл профайла или из профайла импортировать код из шапки, что странно. В итоге код был передвинут, но это там так и осталось: document.querySelector('.header .ava').src = newSrc.

Теперь у нас в коде страницы профайла есть какое-то знание о коде шапки — не хорошо. В начале статьи разобрали, где это может сломаться. Но даже если мы попытаемся применить DDD и выделим код профайла отдельно от страницы профайла, завязка на интерфейс у нас всё равно останется: document.querySelector('.profile .ava').src = newSrc; document.querySelector('.header .ava').src = newSrc, просто будет лежать в другой папочке. Таким образом, отделяя домен бизнесовый, мы раздробили домен системный — код страницы профайла и код шапки. Шило на мыло.

director by robert b weide

▍ Проектирование с реактивным программированием


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

Вспоминая пример с обновлением аватарки в шапке, код выглядел бы так для шапки: profile.$picture.subscribe(src => { this.ava.src = src }), в то время как в модели профайла нужно было бы просто экспортировать синглтон profile с picture, обёрнутым в контейнер Observable — $picture. Теперь код профиля ничего не знает о шапке, но она связана с ним наглядно и типобезопасно. Мы легко можем отследить эти связи при необходимости — Find all references в IDE, но сам код профайла остался максимально чистым и ёмким. По сути ничего не меняется, но код становится обслуживать легче. Повторю КДПВ.

reactivity as a pattern for low models coupling

▍ Ленивость


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

▍ Инвалидация кеша


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

Мы знаем, что связи в нашей системе никуда не делись, но перенеслись из кода в рантайм, и рантаймом этим управляет какой-то утилитарный код. Обычно это интерфейс, который скрывает за собой список подписчиков и логику их обхода при поступлении новой информации, но как ещё это можно использовать? Я долго копаюсь в этой теме и с уверенностью могу сказать, что сложная реактивная система походит на упрощённую реализацию виртуальной машины. Тут и автоматическая очистка мусора (garbage collection), и инлайн кеши (мемоизация), и виртуальная адресация (скоупы / контексты), и ещё небольшая пачка разнообразных фич для метапрограммирования(?). Одна из таких фич лежит на поверхности — зная все места хранения данных и связи между ними, можно легко отслеживать инвалидацию данных и их связей. Главная прелесть такой оптимизации заключается в её автоматическом применении, что делает вопрос производительности всего приложения более предсказуемым.

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

image

▍ Что такое хорошо, что такое плохо


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

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

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

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

В своём канале я опубликовал сравнение реализации очень простой задачи на двух разных реактивных библиотеках. Одна пропагандирует описывать вообще все связи реактивно, вторая подразумевает описание локальных процессов модели императивно. Размер кода отличается, но является не самым страшным моментом. Стрелочки на скрине показывают последовательность чтения кода при дебаге — попытке увидеть последовательность его выполнения (самая крайняя левая стрелка — начало операции). На этом примере очевидно, что процессы, описанные через реактивные интерфейсы, требуют больший путь для чтения — приходится больше прыгать глазами по коду.



В чём же причина, почему в этом примере реактивный подход выглядит так плохо? Давайте вспомним определение, что реактивность — способ разбиения кода. Зачем разбивать код единого процесса? В этом нет никакого смысла. Помимо связанности у нас есть и зацепленность, иногда эти понятия путают, но в английском они строго определены: сoupling и cohesion. На вики есть прекрасная иллюстрация.

image

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

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

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

▍ Выводы


ФРП и Rx помогают описывать асинхронные цепочки процессов, которые можно переиспользовать, но нужно это откровенно редко. При этом Rx очень плох в управлении состоянием — связанными данными. Команда Angular так и не смогла адаптировать rxjs для оптимального управления состоянием и недавно завезла отдельный примитив для этого — сигналы.

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

Оба имеют специфическое апи и связанные с этим проблемы.

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

Выбирайте правильный тулинг, а самое главное — делайте это осознанно. Low coupling для модулей через публичные реактивные интерфейсы. Hight cohesion — через простой императивный код для внутренней логики — процессов.

Telegram-канал с розыгрышами призов, новостями IT и постами о ретроиграх ????️

Комментарии (31)


  1. monochromer
    24.05.2023 13:52

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

    сoupling - связанность

    cohesion - связность

    https://habr.com/ru/articles/568216/

    Поправьте, если ошибаюсь.


    1. artalar Автор
      24.05.2023 13:52
      +1

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

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


  1. markelov69
    24.05.2023 13:52

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

    Пример в студию, что не может решить MobX, но при этом может решить Reatom пожалуйста.


    1. artalar Автор
      24.05.2023 13:52

      Да вариантов много. Вот есть у нас процесс оформления заказа. Отдельно пилится менее важный, но полезный модуль подсказок, который никак с критичным процессом оформления не должен быть связан. Он подгружается лениво, подписывается на событие открытия формы заказа (она может быть открыта в разных местах, в том числе в модалке) и ждет событие завершения заказа, или через 1 минуту (это я сейчас придумал), показывает нотификацию "Вам нужна помощь? Напишите нам в чат!".

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

      Вот еще один очень упрощенный пример:


      1. markelov69
        24.05.2023 13:52

        Да вариантов много.

        Вы так и не привели ни одного варианта. То что вы выдумали, решается с MobX'ом и с Redux'ом и просто на голом JS и т.п. А вообще вы слышали про такое понятие как EventEmitter?) С его помощью можно вообще элементарно решить ваш придуманный пример и без разницы что будет в паре с этим принципом, хоть mobx, хоть redux, хоть rxjs и т.п.

        На реатоме достаточно просто следить за экшенами старта и окончания процесса оформления.

        Без примеров конкретных в коде где обсирается MobX и решает Reatom это просто ваше предположение. Поэтому конкретный пример на codesandbox'e в студию пожалуйста.


        1. artalar Автор
          24.05.2023 13:52
          +3

          Факт 1: на изменение стейта в мобыксе подписаться можно, на вызов экшена нельзя.

          Факт 2: в реатоме можно подписаться и на изменение стейта и на вызов экшена

          Факт 3: с ваших слов, мобыкс может работать только в паре с EventEmitter, те нужно еще где-то его взять. Первоначально вопрос был `что не может решить MobX?`, вы сами на него ответили


          1. markelov69
            24.05.2023 13:52

            Факт 1: на изменение стейта в мобыксе подписаться можно, на вызов экшена нельзя.

            А зачем собственно это нужно?) Это уже событийная модель. Это вообще не связано со стейт менеджментом. Для этого как раз EventEmitter нужен. "Недостаток" - не засчитан.

            Факт 2: в реатоме можно подписаться и на изменение стейта и на вызов экшена

            см. пункт 1. "Преимущество" - не засчитывается.

            Факт 3: с ваших слов, мобыкс может работать только в паре с EventEmitter, те нужно еще где-то его взять. Первоначально вопрос был `что не может решить MobX?`, вы сами на него ответили

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

            те нужно еще где-то его взять

            2 минуты времени написать самому или нагуглить.

            EventEmitter
            export enum EVENT_EMITTERS {
                EXAMPLE = 'EXAMPLE',
            }
            
            export type EVENT_EMITTERS_CALLBACKS_PARAMS = {
                [EVENT_EMITTERS.EXAMPLE]: (param1: string, param2: number, param3?: any) => void;
            };
            
            interface IKeyFuncVal {
                [k: string]: (...args) => any;
            }
            
            export class EventEmitter<Events extends IKeyFuncVal, Key extends keyof Events = keyof Events> {
                logEmits: boolean = false;
                private listeners: Map<Key, Set<Events[Key]>> = new Map();
            
                emit<K extends Key>(eventName: K, ...restParams: Parameters<Events[K]>) {
                    const events = this.listeners.get(eventName);
            
                    if (this.logEmits) {
                        let subscribersCount = 0;
                        if (events) subscribersCount = events.size;
            
                        console.log(`EventEmitter emits: "${String(eventName)}", subscribers: ${subscribersCount}`);
                    }
            
                    if (events) {
                        const eventsArray = Array.from(events);
                        for (const fn of eventsArray) {
                            fn.call(null, ...restParams);
                        }
                    }
                }
            
                on<K extends Key>(eventName: K, fn: Events[K]): () => void {
                    if (!this.listeners.get(eventName)) {
                        this.listeners.set(eventName, new Set());
                    }
            
                    const events = this.listeners.get(eventName);
            
                    events.add(fn);
            
                    // or use unsubscribe function
                    return this.off.bind(this, eventName, fn);
                }
            
                once<K extends Key>(eventName: K, fn: Events[K]): () => void {
                    // @ts-ignore
                    const unsubscribe = this.on(eventName, (...args: any[]) => {
                        fn(...args);
                        unsubscribe();
                    });
            
                    return unsubscribe;
                }
            
                off<K extends Key>(eventName: K, fn: Events[K]) {
                    const events = this.listeners.get(eventName);
            
                    if (events) events.delete(fn);
                }
            }
            
            export const globalEventEmitter = new EventEmitter<EVENT_EMITTERS_CALLBACKS_PARAMS>();
            

            Первоначально вопрос был `что не может решить MobX?`, вы сами на него ответили

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

            Итого резюмируем:
            Вся ваша логика того, что якобы Reatom лучше MobX, хотя на самом деле нет(достаточно просто взглянуть на итоговый код с использованием reatom и mobx и всё становится понятно), строится на том, что в MobX не встроенного EventEmitter'a, который нужен вообще для событий, а не для стейт менеджмента))


            1. artalar Автор
              24.05.2023 13:52
              +2

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

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


              1. markelov69
                24.05.2023 13:52
                -1

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

                Нет, не шире. Нет, не остановимся. Пустые слова не являются аргументами. Т.к. вы почему-то приплетаете событийную модель(EventEmitter) к стейт менджменту.
                Reatom - стейт меннеджер.
                MobX - стейт меннеджер.
                Событийная модель - не стейт менеджмент. Её уже 100 лет в обед и они была задолго, до такого понятия как стейт менеджмент.

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

                Так это событийная модель. При чем тут вообще Reatom, MobX, Redux и т.д. и т.п.??
                events.on('start-form', doSome)
                events.on('finish', doSome);

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


            1. nin-jin
              24.05.2023 13:52

              А вы его зачем велосипедите, когда есть стандартный EventTarget?


              1. markelov69
                24.05.2023 13:52

                Он не очень удобный и нет возможности делать так:

                await eventEmitter.emit('lalla', data);
                // do some after all async work in subscribers


      1. merrick_krg
        24.05.2023 13:52

        А не пробовали предложить в mobx добавить функциональность подписки на экшены, выглядит так, что там не требуется много для этого?


        1. nin-jin
          24.05.2023 13:52

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


          1. artalar Автор
            24.05.2023 13:52

            Иметь разные очереди батчинга - так себе идея.


        1. artalar Автор
          24.05.2023 13:52

          Зачем раздувать этого тормознутого монстра ещё больше :)

          Лично моя нелюбовь к мобыксу обусловлена ещё и неприятием использования прокси - слишком много проблем с ними.


          1. markelov69
            24.05.2023 13:52

            Зачем раздувать этого тормознутого монстра ещё больше :)

            Тормознутого?) Это вы оцениваете по синтетике, а не по приложениям в реальной жизни. Типо ой, 100тыс синхронных операций отработаю на 1ms медленнее, какой ужас и кошмар. Тем более на стороне клиента.
            60 FPS это 16.6ms между каждым кадром.

            Лично моя нелюбовь к мобыксу обусловлена ещё и неприятием использования прокси - слишком много проблем с ними.

            1) Проблем как бы нет, только если выдуманных и высосанных из пальца.
            2) Как бы, есть такая штука, называется конфиг. Вжух, и никаких Proxy нет. Только getter/setters.

            import { configure } from "mobx"
            
            configure({
                useProxies: "never"
            })

            слишком много проблем с ними.

            Каких??? Хоть 1 настоящую проблему назовите, которую можно реально проблемой считать.


  1. nin-jin
    24.05.2023 13:52
    +4

    ОРП тут недоумевает: с чего это реактивный код вдруг стал менее императивным.

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

    В $mol_wire можно:

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

    Какая-то спец олимпиада. Вот так выглядит короткий понятный реактивный код с тем же поведением:


    1. artalar Автор
      24.05.2023 13:52

      В $mol_wire можно

      В реатоме тоже можно :) в некоторых случаях там вывод внутренней структуры покрасивее мола будет. Но это все еще заметно сложнее нативных структур, как можно утверждать что 100 байт читать так же просто как 10?

      короткий понятный реактивный код

      Только этот код с пачкой ошибок. Нет `"Loading post..."`, current должен выставляться после завершения фетчинга.


      1. nin-jin
        24.05.2023 13:52

        Хз, что за нативные структуры такие. Там обычные js объекты с прямыми ссылками друг на друга. Можно по всему приложению гулять, разворачивая их.

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


        1. artalar Автор
          24.05.2023 13:52
          +1

          Диктовать бизнесу дизайн и UX, запрещая использовать текст для описания состояния загрузки - это мощно!

          А current не нужно сразу менять, потому что пока идет загрузка показывается старый список, соответственно со старым индетефикатором. Что будет если лоадинг упадет - будут разные айди и список.


          1. nin-jin
            24.05.2023 13:52

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

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

            3. Индикатор ожидания в виде текста - это плохой UX.

            ---

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


  1. slonopotamus
    24.05.2023 13:52

    Я правильно понимаю, что суть всего этого вашего реактивного программирования в том что UI подписывается на обновления модели? В чём принципиальное отличие от MVC, которой лет так сорок если не больше?


    1. artalar Автор
      24.05.2023 13:52
      +1

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


      1. Paskin
        24.05.2023 13:52
        +2

        Лет 10 назад мы с товарищем проводили митапы по дизайну и архитектуре программ. Мое внимание привлек мужичок лет 60+, который начал смеяться через пару минут после начала и продолжал до самого перерыва. Я решил узнать, что же так его развеселило - ответом было "Мужики, то о чем вы тут спорите - мы в <местный аналог AT&T> решили еще в 70х".
        В достаточно древнем Smalltalk вообще нет понятия "вызов метода", там можно только послать сообщение обьекту - и рантайм вызовет соответствующий обработчик. И @slonopotamus совершенно прав - в классическом MVC или MVVM изменение модели вызывает событие, на которое автоматически реагируют "подписанные" на него элементы View.
        Кстати, основные проблемы реактивного программирования - вообще не те, которые вы упомянули. Проблемы - это невозможность контроля состояния всей системы (например - в виде транзакций), отсутствие уверенности что ваше сообщение будет обработано вообще, один раз и вовремя - и так далее.


        1. artalar Автор
          24.05.2023 13:52

          Это интересно :)
          Только стоит отменить, что транзакции в реатоме уже есть: https://www.reatom.dev/core#ctxschedule


          1. artalar Автор
            24.05.2023 13:52

            Уточню, в реатоме нет полноценных акторов и соотстветствующей изоляции, нет нормальной возможности из родителя перехватывать ошибку и перезапускать актор, но некоторые свойства акторов есть, например возможность извне декларировать лайфсайкл хуки: onConnect, onDisconnect, onUpdate, которые работают не только на прямые, но и косвенные связи (подписки).


          1. Paskin
            24.05.2023 13:52

            Насколько я понимаю - речь идет о браузере. Я же имею в виду более сложные и распределенные системы. Да и упомянутые вами транзакции вряд ли работают с Web workers.


            1. artalar Автор
              24.05.2023 13:52

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


    1. nin-jin
      24.05.2023 13:52
      +1

      Реактивность не про UI вообще, реактивность про инварианты.


      1. Paskin
        24.05.2023 13:52

        "Реактивность позволяет значительно снизить сложность реализации надёжных программ" - это неправда.
        Снизить количество boilerplate-кода - да, улучшить деление на компоненты - тоже. А вот сложность - нет, гуглите например "exactly-once".


        1. nin-jin
          24.05.2023 13:52
          +2

          Вы лучше сами погуглите различие квантора существования от всеобщности.