Substituição ou redutor de nível 80: o caminho da caixa de mudança para as classes

imagem


Sobre o que será?


Vejamos as metamorfoses dos redutores em meus aplicativos Redux / NGRX nos últimos dois anos. Começando com a switch-case seleção oak, continuando com a seleção do objeto por chave e terminando com as classes com decoradores, blackjack e TypeScript. Vamos tentar revisar não apenas a história desse caminho, mas também encontrar alguma relação causal.


Se você e eu fizermos perguntas sobre o descarte de um clichê no Redux / NGRX, este artigo pode ser interessante para você.

Se você já usa a abordagem para selecionar um redutor de um objeto por chave e está farto dele, é possível alternar imediatamente para "Redutores baseados em classe".

Caixa de distribuição de chocolate


Normalmente, a switch-case baunilha, mas me pareceu que isso discriminava seriamente todos os outros tipos de switch-case de switch-case .

Então, vamos dar uma olhada em um problema típico de criação assíncrona de uma entidade, por exemplo, um Jedi.


 const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' const reducerJediInitialState = { loading: false, //   data: [], error: undefined, } const reducerJedi = (state = reducerJediInitialState, action) => { switch (action.type) { case actionTypeJediCreateInit: return { ...state, loading: true, } case actionTypeJediCreateSuccess: return { loading: false, data: [...state.data, action.payload], error: undefined, } case actionTypeJediCreateError: return { ...state, loading: false, error: action.payload, } default: return state } } 

Serei muito franco e admito que nunca usei casos de switch-case na minha prática. Eu gostaria de acreditar que tenho uma lista de razões para isso:


  • switch-case muito fácil de quebrar: você pode esquecer de inserir a break , pode esquecer o default .
  • switch-case muito detalhada.
  • switch-case quase O (n). Isso não é muito importante por si só, porque Redux não se orgulha de um desempenho de tirar o fôlego, mas esse fato enfurece meu conhecedor interior de beleza.

A maneira lógica de pentear tudo isso é oferecida pela documentação oficial do Redux - para escolher um redutor do objeto por chave.


Selecionando um redutor de um objeto por chave


A ideia é simples - cada mudança de estado pode ser descrita por uma função de estado e ação, e cada uma dessas funções possui uma certa chave (campo de type na ação) que corresponde a ela. Porque type é uma string, nada nos impede de descobrir um objeto para todas essas funções, onde a chave é type e o valor é uma função de conversão de estado puro (redutor). Nesse caso, podemos escolher o redutor necessário pela tecla (O (1)), quando uma nova ação chega ao redutor raiz.


 const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' const reducerJediInitialState = { loading: false, data: [], error: undefined, } const reducerJediMap = { [actionTypeJediCreateInit]: (state) => ({ ...state, loading: true, }), [actionTypeJediCreateSuccess]: (state, action) => ({ loading: false, data: [...state.data, action.payload], error: undefined, }), [actionTypeJediCreateError]: (state, action) => ({ ...state, loading: false, error: action.payload, }), } const reducerJedi = (state = reducerJediInitialState, action) => { //    `type`  const reducer = reducerJediMap[action.type] if (!reducer) { //   ,        return state } //        return reducer(state, action) } 

O mais delicioso é que a lógica dentro do reducerJedi permanece a mesma para qualquer redutor, e podemos reutilizá-lo. Existe até uma biblioteca nano de redux-create-reducer para isso .


 import { createReducer } from 'redux-create-reducer' const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' const reducerJediInitialState = { loading: false, data: [], error: undefined, } const reducerJedi = createReducer(reducerJediInitialState, { [actionTypeJediCreateInit]: (state) => ({ ...state, loading: true, }), [actionTypeJediCreateSuccess]: (state, action) => ({ loading: false, data: [...state.data, action.payload], error: undefined, }), [actionTypeJediCreateError]: (state, action) => ({ ...state, loading: false, error: action.payload, }), }) 

Parece que nada aconteceu. É verdade que uma colher de mel não deixa de ter um barril de alcatrão:


  • Para redutores complexos, temos que deixar comentários, porque esse método não fornece uma saída imediata da caixa para fornecer algumas meta-informações explicativas.
  • Objetos com vários redutores e chaves não são bem lidos.
  • Cada redutor tem apenas uma chave. Mas e se você quiser executar o mesmo redutor para vários jogos de ação?

Eu quase caí em lágrimas de felicidade quando me mudei para redutores baseados em classe, e abaixo explicarei o porquê.


Redutores baseados em classe


Pãezinhos:


  • Métodos de classe são nossos redutores e métodos têm nomes. Apenas as informações meta que dizem o que esse redutor faz.
  • Os métodos de classe podem ser decorados, que é uma maneira declarativa simples de vincular redutores e suas ações correspondentes (a saber, ações, não apenas uma ação!)
  • Sob o capô, você pode usar todos os mesmos objetos para obter O (1).

No final, eu gostaria de obter algo assim.


 const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' class ReducerJedi { //     "Class field delcaratrions",    Stage 3. // https://github.com/tc39/proposal-class-fields initialState = { loading: false, data: [], error: undefined, } @Action(actionTypeJediCreateInit) startLoading(state) { return { ...state, loading: true, } } @Action(actionTypeJediCreateSuccess) addNewJedi(state, action) { return { loading: false, data: [...state.data, action.payload], error: undefined, } } @Action(actionTypeJediCreateError) error(state, action) { return { ...state, loading: false, error: action.payload, } } } 

Eu vejo o objetivo, não vejo obstáculos.


Etapa 1. Decorador @Action .


Precisamos que neste decorador possamos colocar qualquer número de jogos de ação e que esses serviços sejam salvos como uma meta-informação, que pode ser acessada posteriormente. Para fazer isso, podemos usar o maravilhoso polyfill reflect-metadata , que corrige o Reflect .


 const METADATA_KEY_ACTION = 'reducer-class-action-metadata' export const Action = (...actionTypes) => (target, propertyKey, descriptor) => { Reflect.defineMetadata(METADATA_KEY_ACTION, actionTypes, target, propertyKey) } 

Etapa 2. Transforme a turma em, de fato, um redutor.


Desenhe um círculo, desenhe um segundo, e agora um pouco de mágica e consiga uma coruja!

Como sabemos, todo redutor é uma função pura que pega o estado e a ação atuais e retorna um novo estado. Uma classe é, obviamente, uma função, mas não exatamente a que precisamos, e as classes ES6 não podem ser chamadas sem new . Em geral, precisamos transformá-lo de alguma forma.


Portanto, precisamos de uma função que leve a classe atual, percorra cada um de seus métodos, colete meta-informações com tipos de ações, colete um objeto com redutores e crie o redutor final a partir desse objeto.


Vamos começar coletando informações meta.


 const getReducerClassMethodsWthActionTypes = (instance) => { //       const proto = Object.getPrototypeOf(instance) const methodNames = Object.getOwnPropertyNames(proto).filter( (name) => name !== 'constructor', ) //             const res = [] methodNames.forEach((methodName) => { const actionTypes = Reflect.getMetadata( METADATA_KEY_ACTION, instance, methodName, ) //     `this`    const method = instance[methodName].bind(instance) //  ,         actionTypes.forEach((actionType) => res.push({ actionType, method, }), ) }) return res } 

Agora podemos converter a coleção resultante em um objeto


 const getReducerMap = (methodsWithActionTypes) => methodsWithActionTypes.reduce((reducerMap, { method, actionType }) => { reducerMap[actionType] = method return reducerMap }, {}) 

Portanto, a função final pode ficar assim:


 import { createReducer } from 'redux-create-reducer' const createClassReducer = (ReducerClass) => { const reducerClass = new ReducerClass() const methodsWithActionTypes = getReducerClassMethodsWthActionTypes( reducerClass, ) const reducerMap = getReducerMap(methodsWithActionTypes) const initialState = reducerClass.initialState const reducer = createReducer(initialState, reducerMap) return reducer } 

Em seguida, podemos aplicá-lo à nossa classe ReducerJedi .


 const reducerJedi = createClassReducer(ReducerJedi) 

Etapa 3. Examinamos o que aconteceu como resultado.


 //       import { Action, createClassReducer } from 'utils/reducer-class' const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' class ReducerJedi { //     "Class field delcaratrions",    Stage 3. // https://github.com/tc39/proposal-class-fields initialState = { loading: false, data: [], error: undefined, } @Action(actionTypeJediCreateInit) startLoading(state) { return { ...state, loading: true, } } @Action(actionTypeJediCreateSuccess) addNewJedi(state, action) { return { loading: false, data: [...state.data, action.payload], error: undefined, } } @Action(actionTypeJediCreateError) error(state, action) { return { ...state, loading: false, error: action.payload, } } } export const reducerJedi = createClassReducer(ReducerJedi) 

Como viver?


Algo que deixamos nos bastidores:


  • E se o mesmo tipo de ação corresponder a vários redutores?
  • Seria ótimo adicionar immer fora da caixa.
  • E se quisermos usar classes para criar nossas ações? Ou funções (criadores de ação)? Gostaria que o decorador fosse capaz de aceitar não apenas tipos de ações, mas também criadores de ações.

Uma pequena biblioteca de classe redutora possui toda essa funcionalidade com exemplos adicionais.


Vale ressaltar que a idéia de usar classes para redutores não é nova. O @amcdnl criou uma grande biblioteca de ações do ngrx , mas parece que ele já o marcou e mudou para o NGXS . Além disso, eu queria uma digitação mais rigorosa e redefinir o lastro na forma específica da funcionalidade Angular. Aqui está uma lista das principais diferenças entre a classe redutora e as ações ngrx.


Se você gostou da ideia de classes para redutores, também pode usar classes para suas ações. Dê uma olhada na classe de ação e fluxo .

Espero que você não tenha perdido tempo em vão, e o artigo tenha sido pelo menos um pouco útil para você. Por favor, chute e critique. Vamos aprender a codificar melhor juntos.

Source: https://habr.com/ru/post/pt440524/


All Articles