Удалите всю свою «бизнес-логику» из магазина Vuex, переместив их в обычные функции.

Эта проблема

Если вы используете библиотеку управления состоянием, такую ​​как Redux или Vuex, вы, вероятно, напишете множество методов «действий», содержащих вашу бизнес-логику. (Эта статья применима как к Vuex, так и к Redux. Но, поскольку я работаю с Vuex в последнее время, я буду приводить примеры на Vuex)

Вот общий упрощенный пример определения магазина в Vuex.

// STORE 
import * as searchApiClient from '@/apiClients/searchApiClient';
... 
{
  namespaced: true,
  state: {
    isSearchInProgress: false,
    searchResults: {},
  },
  getters: {
    [getterNames.resultsCount]: (state) =>
      (state.searchResults.results || []).length,
  },
  mutations: {
    [mutationNames.searchStarted](state) {
      state.isSearchInProgress = true;
      state.searchResults = {};
    },
    [mutationNames.searchCompleted](state, { results }) {
      state.isSearchInProgress = false;
      state.searchResults = results;
    },
  },
  actions: {
    async [actionNames.getSearchResultsAsync](context, { query }) {
      if (!query) {
        return;
      }
      context.commit(mutationNames.searchStarted);
      const results = await searchApiClient.searchAsync(query);
      context.commit(mutationNames.searchCompleted, { results });
    },
  },
}

Чтобы завершить пример, когда у вас есть это хранилище, вы используете его в компоненте Vue следующим образом:

// COMPONENT 
computed: {
  ...mapState(moduleNames.search, {
    searchResults: (state) => state.searchResults.results,
    isSearchInProgress: (state) => state.isSearchInProgress,
  }),
  ...mapGetters(moduleNames.search, [getterNames.resultsCount]),
},
methods: {
  async searchAsync() {
    await this.$store.dispatch(
      `${moduleNames.search}/${actionNames.getSearchResultsAsync}`,
      { query: this.query }
    );
  },
},

Что такое бизнес-логика
Метод async getSearchResultsAsync(context, params) - это ваша бизнес-логика. Это содержит:

* Проверка if (!query) {return;}
* Взаимодействие с внешним API await searchApiClient.searchAsync(query)
* Здесь не отображается, но в основном содержит оркестровку другой бизнес-логики context.dispatch('tracking/trackAsync', ....)

В приведенном выше примере кода представлена ​​диаграмма зависимостей, подобная этой:

Как видите, очень быстро магазин становится «приложением». Скорее всего, вы глубоко и страстно ненавидите государственные библиотеки, но все равно должны ими пользоваться. Поскольку такая организация кода порождает несколько проблем:

  • Сложно читать и следовать
    Вы не вызываете «действие» напрямую, а вместо этого вызываете dispatch method и даете имя своему «методу действия». Вы полагаетесь на строковое соответствие имен. Между вызывающим кодом и вызываемой функцией нет прямой связи. IDE не может помочь вам перейти к действию. Вам нужно выполнить строковый поиск имени и перейти к правильному совпадению. ❌
  • Сложно отлаживать
    То же, что и выше, но при отладке. Стек вызовов запутан дополнительными слоями «трубопроводов». ❌
  • Магазин - это монстр
    Магазин напрямую зависит от клиента внешнего API. Бизнес-логика обычно зависит от доступа к внешнему API и другой бизнес-логики (которая также зависит от доступа к другому внешнему API).
    Когда глобальный объект содержит вашу бизнес-логику , он, естественно, становится зависимым от всего этого.
  • Сложно тестировать
    Вы цените печаль, которую приносит эта проблема «зависимости», когда вы начинаете писать тесты. Попробуйте имитировать свой магазин (или его части) в нетривиальном приложении. Вы начинаете имитировать всю свою систему, даже просто чтобы проверить крошечную функциональность.
  • Сложно организовать
    Бизнес-логика обычно связана с комбинацией различных «подмодулей» вашего «состояния». Поэтому иногда, куда бы вы ни поместили свою бизнес-логику, кажется, что она здесь неуместна. (Подсказка: потому что он принадлежит кому-то другому) ❌

Кроме того, «store» - это ваш большой глобальный одноэлементный объект данных. Он уже содержит все данные в одном объекте. Когда вы вкладываете всю свою логику в этот объект, он становится прекрасным примером дымящегося горячего спагетти / монстра / объекта бога (вы называете его).

Решение

Хранилище - это, по сути, «база данных в памяти», из которой вы пишете и читаете. Библиотеки управления состоянием отлично справляются с задачей подключения механизмов уведомления об изменениях базы данных к компонентам пользовательского интерфейса с помощью mapState и mapGetters. Все эти замечательные механизмы используются, и Vuex делает его особенно простым и интуитивно понятным. Продолжайте использовать их, но прекратите использовать actions. Переместите бизнес-логику на простые старые функции и полностью удалите действия магазина из своей кодовой базы.

Вот как бы это выглядело:

Вот чем мог бы стать приведенный выше пример:

// STORE 
{
  namespaced: true,
  state: () => createInitialState(),
  getters: {
    [getterNames.resultsCount]: (state) =>
      (state.searchResults.results || []).length,
  },
  mutations: {
    [mutationNames.searchStarted](state) {
      state.isSearchInProgress = true;
      state.searchResults = {};
    },
    [mutationNames.searchCompleted](state, { results }) {
      state.isSearchInProgress = false;
      state.searchResults = results;
    },
  },
};

Вы бы определили свою бизнес-логику в обычной функции:

// SERVICE 
import * as searchApiClient from '@/apiClients/searchApiClient';
export async function searchAsync (store, query) {
  if (!!query == false) {
    return;
  }
  store.commit(mutationNames.searchStarted);
  const results = await searchApiClient.searchAsync(query);     
  store.commit(mutationNames.searchCompleted, { results }); 
};

Кодируйте всю свою логику как можно проще и взаимодействуйте с вашим магазином (базой данных) с помощью мутаций (это своего рода шаблон «репозиторий»). Чисто, просто и аккуратно.

Чтобы завершить пример, вы должны использовать магазин и службы в своем компоненте Vue следующим образом:

// COMPONENT 
import * as searchService from '@/services/searchService';
computed: {
  ...mapState(moduleNames.search, {
    searchResults: (state) => state.searchResults.results,
    isSearchInProgress: (state) => state.isSearchInProgress,
  }),
  ...mapGetters(moduleNames.search, [getterNames.resultsCount]),
},
methods: {
  async searchAsync() {
    await searchService.searchAsync(this.$store, this.query);
  },
},

Хорошо, преимущества:

  • Легче читать и следовать
    Нет соответствия строк. Вызов бизнес-логики из компонента прямой. IDE снова ваш друг. Это позволит вам перейти к определению, отобразить IntelliSense и параметры. Все хорошее. Также прямые обращения к другим «методам бизнес-логики». Навигация по коду снова вернулась к своему положению. ✔️
  • Легче отлаживать
    Нет слоев трубопроводов. Звонки прямые. Стек вызовов чистый. ✔️
  • Магазин намного (намного) проще
    Он не зависит от внешних вызовов API или сложных взаимодействий внутри методов действий. (Имейте в виду, если логика вашего бизнеса по своей сути сложна, эта базовая сложность не исчезнет просто потому, что вы переместили ее из одного места в другое 😃 но, по крайней мере, вашим «магазином» будет легче управлять ) ✔️
  • Легче тестировать
    Мы уменьшили хранилище до объекта в памяти без внешней зависимости. Больше не нужно издеваться над магазином. Для каждого теста вы можете создать копию исходного магазина и использовать его напрямую. Когда вам нужно имитировать методы бизнес-логики, вы можете имитировать их более простыми способами, используя стандартные утилиты вашей библиотеки тестирования. ✔️
  • Организовать стало проще
    Теперь, когда функциям бизнес-логики не нужно располагаться в магазине рядом с кучей данных, не стесняйтесь организовывать их любым удобным для вас способом. ✔️

Обратной стороной?

export async function searchAsync (store, query) {

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

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

Вот рабочий пример: https://github.com/veyselozdemir/vuex-no-actions

Заключение

Что ж, большое спасибо за то, что дочитали до этого места. Вы прочитали более 1000 слов. Это достижение 😃

Вывод: не используйте действия.

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

TL; DR

Удалите всю свою «бизнес-логику» из магазина Vuex, переместив их в простые старые функции и удалите все свои action методы из магазина. Ваш код будет легче писать, легче тестировать и, что самое главное, легче читать.

Если вы хотите сразу перейти к образцу: https://github.com/veyselozdemir/vuex-no-actions

Больше контента на plainenglish.io