Как стать автором
Обновить

MobX c MVVM хорош, но с DI ещё лучше

Время на прочтение6 мин
Количество просмотров5.2K

В своей прошлой статье я рассуждал о том, как использование паттерна 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, но может подойти также большинство других библиотек.

Теги:
Хабы:
Всего голосов 4: ↑3 и ↓1+3
Комментарии35

Публикации

Истории

Работа

Ближайшие события

27 августа – 7 октября
Премия digital-кейсов «Проксима»
МоскваОнлайн
28 сентября – 5 октября
О! Хакатон
Онлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн
10 – 11 октября
HR IT & Team Lead конференция «Битва за IT-таланты»
МоскваОнлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн