Когда не стоит использовать ngrx/effects

| Воскресенье, 28 июля, 2019

Метки: Angular, TypeScript Комментарии: 0

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

Функциональное программирование, которое активно применяется в ngrx/store, накладывает некоторые ограничения в реализации нужных нам функций. Но благодаря этому, мы достигаем более адекватного состояния кода снаружи в среде их использования. Речь идет, о так называемых, чистых функциях (pure functions). В ngrx/store редьюсеры, селекторы и rxjs-операторы являются именно такими чистыми функциями.

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

Вспомним, функция считается чистой при следующих условиях:

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

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

Побочные эффекты

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

this.store.dispatch({  
    type: "SAVE_DATA",
    payload: data
});
this.saveData(data) // POST request to server  
    .map(res => this.store.dispatch({type: "DATA_SAVED"}))
    .subscribe((...) => { ... }); 

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

Для этого нам на помощь придет библиотека Ngrx/effects, которая как раз и реализует механизм побочных эффектов в ngrx/store. Она слушает отправляемые actions в rxjs-потоке, выполняет побочные действия, и возвращает новые немедленно исполняемые или асинхронные действия обратно, которые передаются прямо в редьюсер.

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

@Effect() 
saveData$ = this.actions$  
    .ofType('SAVE_DATA')
    .pluck('payload')
    .switchMap(data => this.saveData(data))
    .map(res => ({type: "DATA_SAVED"})) 

Так мы упрощаем разработку компонентов. Только тем, что отправляем действия (dispatch actions) и подписываемся на изменения (subscribe to observables).

Антипатерны с побочными эффектами

Ngrx/effects очень мощное решение, но в тоже время с ним очень легко и нагородить ненужного и запутанного кода. Рассмотрим несколько антипатернов, которые следует избегать и не усложнять себе разработку.

Дублированное или наследованное состояние

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

export interface State { 
    mediaPlaying: boolean; 
    audioPlaying: boolean; 
    videoPlaying: boolean; 
} 

Так как аудио - это тоже мультимедиа, всякий раз, когда audioPlaying имеет значение true, у mediaPlaying должно быть аналогичное значение. Возникает вопрос: «Как синхронизировать это состояние, чтобы при обновлении audioPlaying обновлялся и mediaPlaying?»

Неправильно: использовать эффекты!

@Effect() 
playMediaWithAudio$ = this.actions$ 
    .ofType("PLAY_AUDIO") 
    .map(() => ({type: "PLAY_MEDIA"})) 

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

audioPlaying$ = this.store.select('audioPlaying'); 
videoPlaying$ = this.store.select('videoPlaying'); 
mediaPlaying$ = Observable.combineLatest( 
    this.audioPlaying$, 
    this.videoPlaying$, 
    (audioPlaying, videoPlaying) => audioPlaying || videoPlaying 
) 

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

2. Комбинирование действий и редюсеров

Представьте, что у вас такое дерево состояний:

export interface State { 
    items: {[index: number]: Item}; 
    favoriteItems: number[]; 
} 

Допустим, по бизнес-требованиям нужно иметь возможность удалять элементы из общего списка элементов (items) и из списка отобранных (favoriteItems). Для этого мы создаем два действия DELETE_ITEM_SUCCESS и REMOVE_FAVORITE_ITEM, которые удаляют элементы из соответствующих соcтояний. Но есть одна неувязка, при удалении элемента из общего списка нам нужно удалить его и из отобранных, если он там находится.

Неправильно: использовать эффекты!

@Effect() 
this.actions$
    .ofType("DELETE_ITEM_SUCCESS")
    .map(() => ({type: "REMOVE_FAVORITE_ITEM_ID"})) 

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

Правильно: DELETE_ITEM_SUCCESS может обрабатывать как редюсер элементов, так и редюсер FavoritesItems.

export function favoriteItemsReducer(state = initialState, action: Action)  { 
    switch(action.type)  {
        case 'REMOVE_FAVORITE_ITEM': 
        case 'DELETE_ITEM_SUCCESS':
            const itemId = action.payload;
            return state.filter(id => id !== itemId);
        default:
            return state;  
    }
} 

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

Комментарии
Никто еще не оставил здесь комментарий.
Войдите, чтобы написать комментарий , или воспользуйтесь формой ниже.
 

Copyright © CodeHint.ru 2013-2019