Überschreiben oder Reduzieren der Stufe 80: Der Pfad vom Switch-Case zu den Klassen

Bild


Worum geht es?


Schauen wir uns die Metamorphosen von Reduzierern in meinen Redux / NGRX-Anwendungen in den letzten Jahren an. Beginnend mit dem Eichen- switch-case , weiter mit der Auswahl aus dem Objekt nach Schlüssel und endend mit Klassen mit Dekoratoren, Blackjack und TypeScript. Wir werden versuchen, nicht nur die Geschichte dieses Weges zu überprüfen, sondern auch einen kausalen Zusammenhang zu finden.


Wenn Sie und ich Fragen zur Entsorgung einer Heizplatte in Redux / NGRX stellen, kann dieser Artikel für Sie interessant sein.

Wenn Sie den Ansatz bereits verwenden, um einen Reduzierer aus einem Objekt per Schlüssel auszuwählen, und die Nase voll davon haben, können Sie sofort zu "Klassenbasierte Reduzierer" wechseln.

Schokoladenschalterkoffer


Normalerweise ist der switch-case Vanille, aber es schien mir, dass dies alle anderen Arten von switch-case ernsthaft diskriminierte.

Schauen wir uns also ein typisches Problem der asynchronen Erstellung einer Entität an, 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 zugeben, dass ich in meiner Praxis noch nie einen switch-case habe. Ich würde gerne glauben, dass ich sogar eine Liste von Gründen dafür habe:


  • switch-case zu leicht zu brechen: Sie können vergessen, break einzufügen, Sie können die default vergessen.
  • switch-case zu ausführlich.
  • switch-case fast O (n). Das ist an sich nicht sehr wichtig, weil Redux bietet an sich keine atemberaubende Leistung, aber diese Tatsache macht meinen inneren Schönheitskenner wütend.

Die logische Möglichkeit, all dies zu kämmen, bietet die offizielle Redux-Dokumentation - die Auswahl eines Reduzierers aus dem Objekt per Schlüssel.


Auswahl eines Reduzierers aus einem Objekt per Schlüssel


Die Idee ist einfach: Jede Zustandsänderung kann durch eine Funktion von Zustand und Aktion beschrieben werden, und jede solche Funktion hat einen bestimmten Schlüssel ( type in der Aktion), der ihr entspricht. Weil type ist eine Zeichenfolge, nichts hindert uns daran, ein Objekt für all diese Funktionen zu finden, bei denen der Schlüssel type und der Wert eine reine Zustandsumwandlungsfunktion (Reduzierer) ist. In diesem Fall können wir den erforderlichen Reduzierer per Schlüssel (O (1)) auswählen, wenn eine neue Aktion beim Root-Reduzierer eintrifft.


 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 Köstlichste ist, dass die Logik in reducerJedi für jeden reducerJedi bleibt und wir sie wiederverwenden können. Dafü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, dass nichts passiert ist. Ein Löffel Honig ist zwar nicht ohne ein Fass Teer:


  • Für komplexe Reduzierungen müssen wir Kommentare hinterlassen, weil Diese Methode bietet keinen sofortigen Ausweg, um einige erklärende Metainformationen bereitzustellen.
  • Objekte mit einer Reihe von Reduzierern und Schlüsseln werden nicht gut gelesen.
  • Jeder Reduzierer hat nur einen Schlüssel. Aber was ist, wenn Sie denselben Reduzierer für mehrere Actionspiele verwenden möchten?

Ich brach fast in Tränen des Glücks aus, als ich zu klassenbasierten Reduzierern wechselte, und im Folgenden werde ich erklären, warum.


Klassenbasierte Reduzierstücke


Brötchen:


  • Klassenmethoden sind unsere Reduzierer, und Methoden haben Namen. Nur die Meta-Informationen, die zeigen, was dieser Reduzierer tut.
  • Klassenmethoden können dekoriert werden. Dies ist eine einfache deklarative Methode, um Reduzierungen und ihre entsprechenden Aktionen zu verknüpfen (nämlich Aktionen, nicht nur eine Aktion!).
  • Unter der Haube können Sie dieselben Objekte verwenden, um O (1) zu erhalten.

Am Ende möchte ich so etwas bekommen.


 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. Decorator @Action .


Wir brauchen, dass wir in diesem Dekorateur eine beliebige Anzahl von Aktionen ausführen können und dass diese ZhKshny als eine Art Metainformation gespeichert werden, auf die später zugegriffen werden kann. Dazu können wir die wunderbaren Polyfill - Reflect -Metadaten verwenden , die Reflect patchen .


 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. Verwandeln Sie die Klasse in einen Reduzierer.


Zeichne einen Kreis, zeichne eine Sekunde und jetzt ein bisschen Magie und hol dir 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 benötigen, und ES6-Klassen können nicht ohne new aufgerufen werden. Im Allgemeinen müssen wir es irgendwie transformieren.


Wir brauchen also eine Funktion, die die aktuelle Klasse übernimmt, jede ihrer Methoden durchläuft, Metainformationen mit Aktionstypen sammelt, ein Objekt mit Reduzierungen sammelt und aus diesem Objekt den endgültigen Reduzierer erstellt.


Beginnen wir mit dem Sammeln 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 ReducerJedi Klasse ReducerJedi .


 const reducerJedi = createClassReducer(ReducerJedi) 

Schritt 3. Wir schauen uns an, was als Ergebnis passiert ist.


 //       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 zurückgelassen haben:


  • Was ist, wenn der gleiche Aktionstyp mehreren Reduzierern entspricht?
  • Es wäre toll, immer out of the box hinzuzufügen.
  • Was ist, wenn wir Klassen verwenden möchten, um unsere Aktionen zu erstellen? Oder Funktionen (Aktionsersteller)? Ich möchte, dass der Dekorateur nicht nur Arten von Aktionen akzeptieren kann, sondern auch Aktionsersteller.

Eine kleine Bibliothek der Reduziererklasse bietet all diese Funktionen mit zusätzlichen Beispielen.


Es ist erwähnenswert, dass die Idee, Klassen für Reduzierungen zu verwenden, nicht neu ist. @amcdnl hat einmal eine großartige Bibliothek mit ngrx-Aktionen erstellt , aber es scheint, dass er sie jetzt bewertet und zu NGXS gewechselt hat . Außerdem wollte ich eine strengere Eingabe und das Zurücksetzen des Vorschaltgeräts in Form einer spezifischen Angular-Funktionalität. Hier ist eine Liste der Hauptunterschiede zwischen Reducer-Class- und Ngrx-Aktionen.


Wenn Ihnen die Idee von Klassen für Reduzierungen gefallen hat, möchten Sie möglicherweise auch Klassen für Ihre Aktionen verwenden. Schauen Sie sich die Flux-Action-Klasse an .

Ich hoffe, Sie haben keine Zeit umsonst verschwendet, und der Artikel war zumindest ein wenig nützlich für Sie. Bitte treten und kritisieren. Wir werden lernen, gemeinsam besser zu codieren.

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


All Articles