Level 80 Overagineering oder Raducer: Pfad von Switch-Case zu Klassen

Published on February 16, 2019

Level 80 Overagineering oder Raducer: Pfad von Switch-Case zu Klassen

Ursprünglicher Autor: keenondrums
  • Übersetzung

Bild


Was wird besprochen?


Werfen wir einen Blick auf die Metamorphose des Reduzierers in meinen Redux / NGRX-Anwendungen der letzten Jahre. Ausgehend von der Eiche switch-case, Fortsetzung der Auswahl aus dem Objekt durch Schlüssel und Ende der Klassen mit Dekorateuren, Blackjack und TypeScript. Wir werden versuchen, nicht nur die Geschichte dieses Pfades zu überprüfen, sondern auch einen Kausalzusammenhang zu finden.


Wenn Sie derselbe sind, den ich mich frage, wie man ein Boilerplate in Redux / NGRX loswird, dann könnte Sie dieser Artikel interessieren .

Wenn Sie bereits den Ansatz zur Auswahl eines Reduzierers anhand eines Schlüssels aus einem Objekt verwenden und es satt haben, können Sie sofort zu "Klassenreduzierten Empfängern" blättern.

Schalteretui aus Schokolade


Normalerweise switch-caseVanille, aber es schien mir, dass dies alle anderen Typen ernsthaft diskriminierte switch-case.

Betrachten Sie also das typische Problem der asynchronen Erstellung einer Entität, beispielsweise eines 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
  }
}

Ich werde sehr offen sein und ich gebe zu, dass ich es in meiner Praxis nie benutzt habe switch-case. Ich würde gerne glauben, dass ich dafür sogar eine Liste von Gründen habe:


  • switch-casezu leicht zu brechen: Sie können das Einfügen vergessen break, Sie können es vergessen default.
  • switch-case zu ausführlich.
  • switch-case fast O (n). Es ist an sich nicht so wichtig, weil Redux bietet an sich keine großartige Leistung, aber diese Tatsache macht meinen inneren Kenner der Schönheit extrem wütend.

Die logische Möglichkeit, all dies zu kämmen, bietet die offizielle Redux-Dokumentation : Wählen Sie einen Reduzierer anhand eines Schlüssels aus einem Objekt.


Auswahl eines Reduzierers aus einem Objekt mit der Taste


Die Idee ist einfach - jede Änderung des Zustands kann durch eine Funktion des Zustands und der Aktion beschrieben werden, und jede solche Funktion hat einen bestimmten Schlüssel (Feld typein der Aktion), der ihr entspricht. Weil type- eine Zeichenkette, nichts hindert uns daran, ein Objekt für all diese Funktionen zu verstehen, wobei der Schlüssel dies ist typeund der Wert eine reine Funktion der Zustandsumwandlung (Reduzierer) ist. In diesem Fall können wir den erforderlichen Reduzierer mit der Taste (O (1)) auswählen, wenn eine neue Aktion zum Wurzelreduzierer kommt.


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)
}

Das Leckerste dabei ist, dass die Logik im Inneren reducerJedifür alle Reduzierer gleich bleibt und wir sie wiederverwenden können. Hierfür gibt es sogar eine Redux-Create-Reducer-Nano-Bibliothek .


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,
  }),
})

Es scheint nichts dergleichen passiert zu sein. Richtig, ein Löffel Honig ist nicht ohne Teerfaß:


  • Für komplexe Reduser müssen wir Kommentare hinterlassen, weil Diese Methode bietet keinen sofort einsatzbereiten Weg, um einige erklärende Metainformationen bereitzustellen.
  • Objekte mit vielen Reduzierstücken und Schlüsseln sind nicht gut lesbar.
  • Jeder Reduzierer entspricht nur einer Taste. Und was ist, wenn Sie für mehrere Actionspiele den gleichen Reduzierer verwenden möchten?

Ich brach beinahe in Tränen des Glücks aus, als ich auf der Grundlage von Klassen zu klassenbasiertem Reyuser wechselte, und ich werde Ihnen im Folgenden erklären, warum.


Klassenbasierte Reduzierungen


Brötchen:


  • Klassenmethoden sind unsere Reduktionen, und Methoden haben Namen. Nur die Meta-Informationen, die Ihnen sagen, was dieser Reduzierer tut.
  • Klassenmethoden können dekoriert werden. Dies ist eine einfache deklarative Methode, um die Untersetzungsgetriebe und ihre entsprechenden Aktionen zu verknüpfen (es ist eine Aktion, keine Aktion!).
  • Unter der Haube kannst du dieselben Objekte benutzen, um O (1) zu erhalten.

Am Ende möchte ich so etwas haben.


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,
    }
  }
}

Ich sehe das Ziel, ich sehe keine Hindernisse.


Schritt 1. Dekorateur @Action.


Wir müssen in diesem Decorator eine beliebige Anzahl von Aktionen ausführen und diese als Metainformationen speichern, auf die später zugegriffen werden kann. Dazu können wir die wunderbare Reflect -Metadaten- Polyfüllung verwenden, die Reflect patcht .


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

Schritt 2. Wir machen die Klasse tatsächlich zu einem Reduzierer.


Wir haben einen Kreis gezeichnet, einen zweiten, und jetzt ein bisschen Magie und wir bekommen eine Eule!

Wie wir wissen, ist jeder Reduzierer eine reine Funktion, die den aktuellen Status und die Aktion übernimmt und einen neuen Status zurückgibt. Eine Klasse ist natürlich eine Funktion, aber nicht genau die, die wir brauchen, und die ES6-Klassen können nicht ohne aufgerufen werden new. Im Allgemeinen ist es notwendig, es irgendwie zu transformieren.


Wir brauchen also eine Funktion, die die aktuelle Klasse akzeptiert, jede ihrer Methoden durchläuft, Metainformationen mit Aktionstypen sammelt, ein Objekt mit Reduktionszahnrädern sammelt und aus diesem Objekt ein endgültiges Reduktionszahnrad erstellt.


Beginnen wir mit der Sammlung von Metainformationen.


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
}

Jetzt können wir die resultierende Sammlung in ein Objekt konvertieren.


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

Die endgültige Funktion könnte also so aussehen:


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
}

Als nächstes können wir es auf unsere Klasse anwenden ReducerJedi.


const reducerJedi = createClassReducer(ReducerJedi)

Schritt 3. Das hat sich als Ergebnis herausgestellt.


// Переместим общий код в отдельный модуль
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)

Wie kann man weiterleben?


Etwas, das wir hinter den Kulissen gelassen haben:


  • Was ist, wenn derselbe Aktionstyp mit mehreren Reduzierungen übereinstimmt?
  • Es wäre toll, immer aus der Box hinzuzufügen .
  • Was ist, wenn wir Klassen verwenden möchten, um unsere Actionspiele zu erstellen? Oder Funktionen (Aktionsersteller)? Ich wünschte, der Dekorateur könnte nicht nur die Arten von Aktionen akzeptieren, sondern auch die Macher von Aktionen.

All diese Funktionen mit zusätzlichen Beispielen haben eine kleine Bibliothek Minderer-Klasse .


Es ist erwähnenswert, dass die Idee, Klassen für Reduzierungen zu verwenden, nicht neu ist. @amcdnl hat einmal die großartige ngrx-actions- Bibliothek erstellt , aber es scheint, dass er sie jetzt bewertet und auf NGXS umgestellt hat . Außerdem wollte ich eine strengere Eingabe und ein Zurücksetzen des Vorschaltgeräts als Angular-spezifische Funktionalität. Hier finden Sie eine Liste der wichtigsten Unterschiede zwischen der Reduktionsklasse und den ngrx-Aktionen.


Wenn Sie die Idee von Klassen für die Reduzierer mögen, können Sie die Klassen auch für Ihre Actionspiele verwenden. Schauen Sie sich die Flux-Action-Klasse an .

Ich hoffe, Sie haben Ihre Zeit nicht verschwendet, und der Artikel war nur ein wenig nützlich für Sie. Bitte treten und kritisieren. Wir werden lernen, gemeinsam besser zu programmieren.