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

Дисклеймер

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

Описание проблемы

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

В MVVM стор создается для компонента и его детей. Размер компонента может быть любой - компонент всего приложения, определенная страница, её дочерний компонент и т.п., - но компонент быть обязан. Безусловно, в таких условиях можно хранить общие данные на общем уровне - например, в сторе компонента всего приложения. Однако, в таком подходе есть серьезное "Но" - стор может превратится общую солянку - в здоровенный объект, который сложно анализировать, и в котором нет никакой абстракции данных. Что уже попахивает подходом Redux, и от чего я так рьяно пытаюсь уйти.

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

Решение проблемы - DI

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

Использование этого паттерна в Redux приложениях представить крайне затруднительно - все-таки без ООП им не воспользуешься. В MobX же с этим проблем нет, т.к. каждый стор может быть классом. Классом также является и вьюмодель в подходе MVVM. Поэтому благодаря DI во вьюмодели можно внедрять зависимые классы, которые могут быть сторами.

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

Граф реакт-компонент
Взаимодействие DI и MVVM
Взаимодействие DI и MVVM

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

Пример использования

А теперь давайте я покажу, как это может выглядеть в коде

Пример связки MVVM + DI
UserStore.ts
import { singleton } from 'tsyringe';
import { action, observable, makeObservable } from 'mobx';
import axios from 'axios';

type TLoginDTO = {
  username: string;
}

@singleton()
export class UserStore {
  @observable isLogged = false;

  @observable username = '';

  constructor() {
    makeObservable(this);
  }

  // Other user fields

  @action login = async (username: string, password: string) => {
    const data = await axios.post<unknown, TLoginDTO>('/login', { username, password });
    this.isLogged = true;
    this.username = data.username;
  };
}

LoginPage.tsx
import { injectable } from 'tsyringe';
import { makeObservable, observable } from 'mobx';
import { view, ViewModel } from '@yoskutik/react-vvm';
import { Input, Button } from './components';
import { UserStore } from './UserStore';

@injectable()
class LoginPageViewModal extends ViewModel {
  @observable username = '';

  @observable password = '';

  constructor(private user: UserStore) {
    super();
    makeObservable(this);
  }

  onLoginClick = () => {
    this.user.login(this.username, this.password);
  }
}

const LoginPage = view(LoginPageViewModal)(({ viewModel }) => (
  <div>
    <h2>Login Page</h2>
    <Input type="text" label="Имя пользователя" object={viewModel} field="username" />
    <Input type="password" label="Пароль" object={viewModel} field="password" />
    <Button onClick={viewModel.onLoginClick}>Войти</Button>
  </div>
));

OtherPage.tsx
import { injectable } from 'tsyringe';
import { computed, makeObservable } from 'mobx';
import { view, ViewModel } from '@yoskutik/react-vvm';
import { UserStore } from './UserStore';

@injectable()
class OtherPageViewModal extends ViewModel {
  @computed get name(): string {
    return this.user.isLogged ? this.user.username : 'Странник';
  }
  
  constructor(private user: UserStore) {
    super();
    makeObservable(this);
  }
}

const OtherPage = view(OtherPageViewModal)(({ viewModel }) => (
  <div>
    <h2>Other Page</h2>
    <h3>
      {`Привет, ${viewModel.name}!`}
    </h3>
  </div>
));

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

Описание связки MVVM + DI

Связку MobX + MVVM + DI я считаю ультимативным решением. Невероятно удобным и не создающим никаких новых абстракций. Благодаря всего двум паттернам можно в разы меньше думать об архитектуре приложения. И при этом всего двух паттернов достаточно чтобы покрыть большинство потребностей при разработке.

Я специально отложил описание правил применения DI совместно с MVVM, т.к. для начала хотел вас заинтересовать. Надеюсь, у меня это удалось, потому давайте перейдем к описанию правил использования связки паттернов MVVM и DI.

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

  • Данные, необходимые для отображения разных компонент можно хранить в:

    • В родительской вьюмодели, если вложенность вью не слишком большая. Об этом я расскажу далее.

    • В отдельном DI-контейнере в иных случаях.

  • В большинстве случаев DI-контейнеры должны быть синглтон-классами.

  • В большинстве случаев вьюмодели должны быть транзиентными классами.

Большая вложенность вью

Что же скрывается за этими словами?

Когда вью находится внутри другого вью, он создает собственный объект вьюмодели. Однако, как я говорил ранее вьюмодель должна быть доступна для дочерних компонент, даже если они сами по себе являются вью. В таких случаях вьюмодель родительского вью будет родительской вьюмоделью дочернего вью. И обращаться к ней из дочерней вьюмодели можно будет с помощью this.parent (об этом я более подробно писал в прошлой статье). Соответственно, если нужен родитель родителя parent нужно будет вызвать 2 раза. Если его родитель - 3. И так далее.

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

DI контейнеры должны быть синглтонами

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

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

Вьюмодели должны быть транзиентными

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

Конец

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

Если вы хотите самостоятельно поиграться с подходом MVVM и DI, то можете воспользоваться моей небольшой библиотекой. Вот ссылки: npmgithubдокументация. А ещё можете посмотреть на пару примеров её использования. Кстати, зависимости от определенного DI фреймворка в моей реализации нет. В статье я использовал TSyringe, но может подойти также большинство других библиотек.

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


  1. markelov69
    15.12.2022 11:31

    возникает проблема излишней свободы

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



    Самое главное приемущество у MobX по мимо того, что он использует getters/setter для автоматической подписки/отписки на изменения это его свобода. А вы выдаёте его главное достоинство за недостаток.
    Я думаю вам с такой логикой нужно вообще в другую сторону пойти, в сторону Angular, вот там вам руки свяжут по швам конкретно и будете радоваться жизни в отсутствии свободы. Там и DI как вы любите и много прочей нечисти.


    1. Yoskutik Автор
      15.12.2022 11:44

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

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


      1. markelov69
        15.12.2022 11:56

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

        Читал

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

        Да? Правда? Кроме слова "недостаток" и каких-то выдуманных кейсов не имеющих отношения к реальности и ни одного реального недостатка не видел и не встречал за 6 лет использования связки React + MobX во множестве проектов. Возможно потому что и реальных недостатков то и нет? Кривые руки разработчиков к недостаткам технологии X, Y и т.п. относить нельзя, это сугубо недостатки человеческого характера.

        Вот посмотрите как всё элементарно, и не загрязнено DI и т.п.
        https://codesandbox.io/s/green-moon-vujxxv?file=/src/App.tsx


        1. Yoskutik Автор
          15.12.2022 12:02
          +1

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

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


          1. markelov69
            15.12.2022 12:16
            -1

            и вы удивитесь, тому как часто всплывает тезис "В MobX слишком много свободы".

            И что? Пусть идут лесом, т.к. это пустые слова. Тот кто реально так считает, скорее всего и не разработчик вовсе, а тот кто пришел в профессию исключительно из-за зарплаты. Разработка это целое творчество, а не работа за станком где задача А решается только одним спобсбом и никак иначе.

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

            Так им пофиг, у них либо Redux(и/или его производные) головного мозга, либо Angular/RxJS головного мозга, эти болезни неизлечимы.

            И опять же повторюсь, моя статья направлена как раз на таких людей.

            А смысл? Вы просто подаете очень плохой пример как работать с MobX и таким образом вводите в заблуждение нормальных разработчиков. Зачем-то усложняете элементарные и очевидные вещи на ровном месте.


            1. Yoskutik Автор
              15.12.2022 12:21

              Значит в конечном итоге вы считаете, что описываемый мной подход плох. С этого и стоило начинать. Вы бы не могли описать, чем он плох?


        1. Yoskutik Автор
          15.12.2022 12:11
          +1

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


          1. markelov69
            15.12.2022 12:28
            -1

            Так весь ваш описанный подход сводится к тому, как в компоненте получить состояние MobX'овского класса, просто в извращенном виде. А я просто показал как это делается если вы не относитесь к числу программистов-извращенцев)

            В огромных проектах, я точно так же просто делаю import состояния и читаю его в компонентах, а если в проекте будет 1 billion lines of code, то в этом плане всё равно ничего не изменится, import { someState } from 'lalal' и вперед.

            Тут же сразу отвечу на ваш коммент:

            Значит в конечном итоге вы считаете, что описываемый мной подход плох. С этого и стоило начинать. Вы бы не могли описать, чем он плох?


            Напишите в ответе код компонента который работает с 3мя глобальными состояниями и с одним своим локальным. Как будет выглядеть ваш
            view(OtherPageViewModal)(({ viewModel }) => ( и хак в стиле создать специальный класс, который с агрегирует все эти состояния в себе не применять, т.к. он дополнительно убивает самое главное правило кода - наглядность и очевидность.


            1. Yoskutik Автор
              15.12.2022 12:36

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


              1. markelov69
                15.12.2022 12:40

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

                Далее цитата из моего предыдущего коммента:

                Напишите в ответе код компонента который работает с 3мя глобальными состояниями и с одним своим локальным. Как будет выглядеть вашview(OtherPageViewModal)(({ viewModel }) => ( и хак в стиле создать специальный класс, который с агрегирует все эти состояния в себе не применять


                1. Yoskutik Автор
                  15.12.2022 12:50

                  Формулировка слишком расплывчатая, но да ладно.

                  import { injectable } from 'tsyringe';
                  import { computed, makeObservable } from 'mobx';
                  import { view, ViewModel } from '@yoskutik/react-vvm';
                  import { SecurityService, LicenseService, LICENSE_UPDATE } from '@services';
                  import { UserStore } from './UserStore';
                  
                  @injectable()
                  class OtherPageViewModal extends ViewModel {
                    // Допустим, это локальное состояние, лень придумывать
                    @computed get name(): string {
                      return this.user.isLogged ? this.user.username : 'Странник';
                    }
                    
                    constructor(
                      private user: UserStore,
                      public security: SecurityService,
                      private license: LicenseService,
                    ) {
                      super();
                      makeObservable(this);
                    }
                  
                    onLicenseUpdateClick = () => {
                      this.license.update();
                    }
                  }
                  
                  const OtherPage = view(OtherPageViewModal)(({ viewModel }) => (
                    <div>
                      <h2>Other Page</h2>
                      <h3>
                        {`Привет, ${viewModel.name}!`}
                      </h3>
                      <span>
                        Ваш номер лицензии:
                        {viewModel.license.number}
                      </span>
                  
                      {viewModel.security.isAllowed(LICENSE_UPDATE) && (
                        <button onClick={viewModel.onLicenseUpdateClick}>
                          Обновить лицензию
                        </button>
                      )}
                    </div>
                  ));


                  1. markelov69
                    15.12.2022 14:07

                    Я же написал

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

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

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

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

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

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

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


                    1. Yoskutik Автор
                      15.12.2022 14:46
                      +1

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

                      По мне так читаемость компонента сильно возрастает, когда он состоит исключительно из JSX кода. Операция "залезть в класс обертку" занимает меньше полу секунды с IDE.

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


                      1. markelov69
                        15.12.2022 14:58
                        +1

                        ваш, в котором, кстати, нет никаких 3 глобальных состояний и одного локального.

                        Правда что ли? Вот стрелочки, смотрите, так видно? Или всё ещё состояний нет?

                        А че, так можно было что ли?)
                        А че, так можно было что ли?)

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

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

                        Операция "залезть в класс обертку" занимает меньше полу секунды с IDE.

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


                      1. Yoskutik Автор
                        15.12.2022 15:04

                        Да, про 3 состояния все же не прав, признаю.

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


                      1. markelov69
                        15.12.2022 15:10

                        Ну опять же. 2 файла по 400 строчек или один на 800

                        При чем тут кол-во строк и файлов, и вообще code splitting, если все крутится вокруг того, как компонент получает стейты. Речь про это, а не про 800 строк vs 400 строк. Вы используете DI и промежуточный класс агрегатор, опять же просто так, по приколу. А я просто делаю Import и использую в явном виде.

                        Да, про 3 состояния все же не прав, признаю.

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


        1. nin-jin
          15.12.2022 19:16
          -1

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


          1. markelov69
            15.12.2022 19:42
            -1

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

            Да тут нечего стесняться, я же реалист) Я не борюсь с выдуманными, преувеличенными или высосанными из пальца "проблемами")

            1) Кто сказал что судьба каждого компонента быть переиспользованным? Это участь лишь малой части компонентов в проекте.
            2) Кто сказал что компонент который используется большем чем в одном месте не может читать глобальное состояние приложения?

            P.S. Тут речь про разработку web приложения, а не UI кита, который точно ничего не должен знать о глобальном состоянии.


            1. nin-jin
              16.12.2022 05:29

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


              1. markelov69
                16.12.2022 10:31
                -1

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


  1. zevvssibirix
    15.12.2022 16:59

    Спасибо за материал. Чувствуется глубокое погружение в тему. Почему не используете MST? Да, там болтливый синтаксис описания сторов. Но озвученная вами проблема совместного использования данных решена. До кучи — time travel и снапшоты из коробки — легко подключать API.


    1. markelov69
      15.12.2022 17:41

      Почему не используете MST?

      Наверное что бы НЕ убить всю прелесть MobX'a, чтобы НЕ убить производительность, чтобы НЕ убить код, чтобы приложение не упало если в MST ждём например number, а с бэка пришел string или не пришло поле вовсе, и т.д и т.п.

      Да, там болтливый синтаксис описания сторов. 

      Вот и нафиг оно не упало, как бы Typescript есть.

      До кучи — time travel и снапшоты из коробки

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

      Чувствуется глубокое погружение в тему.

      Ну как сказать)) Больше похоже на услышал звон, не знаю где он)


    1. Yoskutik Автор
      15.12.2022 20:35

      @markelov69, конечно, весьма резко выразился. Однако, я не могу с ним не согласиться. Связка MobX и TypeScript покрывает большинство реальных потребностей, которые постарался закрыть MobX State Tree.

      Необходимость в time travel для меня стоит под сомнением. Особенно, когда у вас несколько десятков, а то и сотен сторов. Мне кажется, разработчики MST просто хотели реализовать фичу, которая была возможна в Redux. Однако, в MobX она мне полезной не кажется.

      А снапшоты мне кажутся слишком неутилитарной функциональностью. Будто далеко не всем они в принципе могут пригодиться.

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


      1. zevvssibirix
        16.12.2022 01:05
        +1

        MST не имеет смысла, если не нужен time travel (сайтики). Но сложно представить себе приложения, вроде google docs, mirro или figma, где time travel был бы отломан. Пользователи не поймут. Поэтому в первую очередь, выбор инструмента зависит от задачи. У меня есть опыт рефакторинга https://singularity-app.ru/, где cmd+z — must have и уже была реализована на redux.

        Ощущения от MST такие:

        Первые: "бля, что за черная магия вне Хогвардса?". После редакса код получался слишком простым. Ты просто пишешь логику приложения, а не все эти сраные экшин-криэйторы. Как так то? В общем, программисты ненавидят черную магию. По крайней мере, пока в ней не разберутся. Почитав исходники — стало понятно, как это все устроено. В прочем, авторы MobX и MST нифига не эстеты по коду, тут им не респект.

        Вторые. Ладно, с черной магией разобрались. Что там со скоростью? Были сомнения, что на большом количестве сторов с большим объемом данных, производительность можно будет обнять и плакать. Но нет. Реальные замеры показали что у нас все ОК. И хотя MST чего-то там делает, требует каких-то накладных расходов (по сравнению с редаксом), но это все — копейки. По итогу рефакторинга, скорость только росла. Пользователи заметили. В прочем, я связываю это не с MST, а с рефакторингом селекторов и мидлварей. Запишем, что со скоростью все ОК.

        Теперь о минусах. Их было три. И все они были на поверхности.

        1. Болтливое описание типов в сторах. "Ну блин, нахрена ж вы так сделали?". Авторам нет оправдания :)) Не, я понимаю, почему они сделали именно так. Но это не круто. В идеальном мире я хочу просто перенаследовать модели с бэкэнда и сделать их обсерверными. Но MST говорит "обрыбишься".

        2. Наследование сторов через трамбу-лямбу (композицию). Блевотка же. Этот минус следует из первого. На простых приложениях это не потребуется. Но если много actions / views — распилить стор по слоям очень захочется. MST по сути предлагает решение. Но оно не эстетичное.

        3. Асинхронные экшины через генераторы. Вот за это хочется бить палками. Благо, таких операций у нас получилось всего 4-5 (CRUD) и их аккуратно спрятали с глаз долой в базовую модель. Но смотреть на flow(function* fetchProjects()... у меня глаз дергается. Могли бы напрячься, и починить обычные промисы/асинк/авэйт.

        Что запишу в плюсы (они все спорные, но для меня — плюсы, поэтому кто не согласен — просто отвалите):

        1. Код реально получается ОЧЕНЬ простой. Как и архитектура. И кода становится в разы меньше, чем на redux. Меньше кода — меньше багов. Разработка ускорилась.

        2. Тестируемость. По сути, в тестах мы замокали один объект (backend-connector), передаваемый в mst через депенденси-энджекшин. И получили простые, чистые, красивые юнит-тесты на селекторы и экшины. Я — доволен.

        3. Таймтревел и снапшоты. В нашем случае это must have. Порадовала работа с патчами.

        Чем не стали пользоваться: в MST есть коннектор к Redux-стору. По сути — тупая мидлварь на два экрана. Были мыслишки, что для рефакторинга это будет удобно. По факту удобнее оказалось извлекать какю-то сущьность из редакса целиком.

        Итого: если тайм тревел не нужен — вам не нужен MST. Если нужен — на чистом MobX его рехнешься писать. Ну а redux, безусловно, нужно потихоньку хоронить.


        1. nin-jin
          16.12.2022 05:58
          +1

          Но сложно представить себе приложения, вроде google docs, mirro или figma, где time travel был бы отломан. Пользователи не поймут. 

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

          Могли бы напрячься, и починить обычные промисы/асинк/авэйт.

          На уровне js это не починить. Тут надо спецификацию языка менять.

          И получили простые, чистые, красивые юнит-тесты на селекторы и экшины. Я — доволен.

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

          если тайм тревел не нужен — вам не нужен MST. Если нужен — на чистом MobX его рехнешься писать. 

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


          1. zevvssibirix
            16.12.2022 09:32

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

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

            То есть пользовательские сценарии у вас не тестируются, ясно. 

            С чего вы взяли? Где написано, что "юнитами все и кончилось". Бред не пишите. У нас все уровни покрыты.


            1. nin-jin
              16.12.2022 10:25
              +1

              Вы бы сначала спеки MST почитали, прежде чем такую дичь морозить.

              https://mobx-state-tree.js.org/intro/getting-started#time-travel

              У нас все уровни покрыты.

              Знаем мы эти "все уровни покрыты".