Reduziererorganisation - einen Schritt weiter gehen


Was werden wir hier behandeln?


Wir werden einen Überblick über die Entwicklung der Reduzierungen in meinen Redux / NGRX-Apps geben, die in den letzten zwei Jahren stattgefunden haben. Ausgehend vom Vanille- switch-case ein Reduzierer aus einem Objekt per Schlüssel ausgewählt und schließlich mit klassenbasierten Reduzierern entschieden. Wir werden nicht nur darüber sprechen, wie, sondern auch darüber, warum.


Wenn Sie daran interessiert sind, zu viel Boilerplate in Redux / NGRX zu umgehen, sollten Sie diesen Artikel lesen.

Wenn Sie bereits mit der Auswahl eines Reduzierers aus einer Kartentechnik vertraut sind, sollten Sie direkt zu klassenbasierten Reduzierern springen.

Vanille-Schaltergehäuse


Schauen wir uns also eine alltägliche Aufgabe an, bei der asynchron eine Entität auf dem Server erstellt wird. Dieses Mal schlage ich vor, wir beschreiben, wie wir einen neuen Jedi erschaffen können.


 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, // List of our jedi 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 } } 

Lassen Sie mich ehrlich sein, ich habe diese Art von Reduzierstücken noch nie in der Produktion verwendet. Meine Argumentation ist dreifach:


  • switch-case führt einige Spannungspunkte und undichte Rohre ein, die wir möglicherweise irgendwann vergessen, rechtzeitig zu reparieren. Wir könnten immer vergessen, eine break wenn wir nicht sofort return . Wir könnten immer vergessen, default hinzuzufügen, das wir jedem Reduzierer hinzufügen müssen.
  • switch-case hat selbst einen Boilerplate-Code, der keinen Kontext hinzufügt.
  • switch-case ist O (n), Art von . Es ist kein solides Argument für sich, weil Redux sowieso nicht sehr performant ist, aber es macht meinen inneren Perfektionisten wütend.

Der logische nächste Schritt, den die offizielle Dokumentation von Redux vorschlägt, besteht darin, einen Reduzierer anhand eines Schlüssels aus einem Objekt auszuwählen.


Auswahl eines Reduzierers aus einem Objekt per Schlüssel


Die Idee ist einfach. Jede Zustandstransformation ist eine Funktion aus Zustand und Aktion und hat einen entsprechenden Aktionstyp. Wenn man bedenkt, dass jeder Aktionstyp eine Zeichenfolge ist, können wir ein Objekt erstellen, wobei jeder Schlüssel ein Aktionstyp ist und jeder Wert eine Funktion ist, die den Status transformiert (ein Reduzierer). Dann könnten wir einen erforderlichen Reduzierer aus diesem Objekt per Schlüssel auswählen, nämlich O (1), wenn wir eine neue Aktion erhalten.


 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) => { // Pick a reducer by action type const reducer = reducerJediMap[action.type] if (!reducer) { // Return state unchanged if we did not find a suitable reducer return state } // Run suitable reducer if found one return reducer(state, action) } 

Das Coole dabei ist, dass die Logik im reducerJedi für jedes Reduzierstück gleich bleibt, was bedeutet, dass wir es wiederverwenden können. Es gibt sogar eine kleine Bibliothek namens Redux-Create-Reducer , die genau das tut. Dadurch sieht der Code folgendermaßen aus:


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

Schön und hübsch, oder? Obwohl dies hübsch noch ein paar Einschränkungen hat:


  • Bei komplexen Reduzierern müssen wir viele Kommentare hinterlassen, die beschreiben, was dieser Reduzierer tut und warum.
  • Riesige Reduzierkarten sind schwer zu lesen.
  • Jeder Reduzierer hat nur einen entsprechenden Aktionstyp. Was ist, wenn ich denselben Reduzierer für mehrere Aktionen ausführen möchte?

Klassenbasierter Reduzierer wurde zu meinem Lichtblick im Königreich der Nacht.


Klassenbasierte Reduzierungen


Lassen Sie mich diesmal mit dem Warum dieses Ansatzes beginnen:


  • Die Methoden der Klasse werden unsere Reduzierer sein und die Methoden haben Namen, was eine nützliche Metainformation ist, und wir könnten in 90% der Fälle Kommentare aufgeben.
  • Die Methoden der Klasse könnten dekoriert werden, was eine leicht zu lesende deklarative Methode ist, um Aktionen und Reduzierungen abzugleichen.
  • Wir könnten immer noch eine Karte von Aktionen unter der Haube verwenden, um O (1) -Komplexität zu haben.

Wenn das nach einer vernünftigen Liste von Gründen für Sie klingt, lassen Sie uns eintauchen!


Zunächst möchte ich definieren, was wir als Ergebnis erhalten möchten.


 const actionTypeJediCreateInit = 'jedi-app/jedi-create-init' const actionTypeJediCreateSuccess = 'jedi-app/jedi-create-success' const actionTypeJediCreateError = 'jedi-app/jedi-create-error' class ReducerJedi { // Take a look at "Class field delcaratrions" proposal, which is now at 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, } } } 

Jetzt, da wir sehen, wo wir hin wollen, können wir es Schritt für Schritt tun.


Schritt 1. Aktionsdekorateur .


Was wir hier tun möchten, ist, eine beliebige Anzahl von Aktionstypen zu akzeptieren und diese als Metainformationen für die Methode einer Klasse zu speichern, die später verwendet werden soll. Zu diesem Zweck könnten wir die Reflect -Metadaten- Polyfüllung verwenden, die dem Reflect- Objekt Metadatenfunktionalität verleiht. Danach würde dieser Dekorateur seine Argumente (Aktionstypen) einfach als Metadaten an eine Methode anhängen.


 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. Erstellen einer Reduzierungsfunktion aus einer Reduzierklasse


Wie wir wissen, ist jeder Reduzierer eine reine Funktion, die einen Zustand und eine Aktion akzeptiert und einen neuen Zustand zurückgibt. Nun, Klasse ist auch eine Funktion, aber ES6-Klassen können nicht ohne new aufgerufen werden, und wir müssen ohnehin mit ein paar Methoden einen tatsächlichen Reduzierer aus einer Klasse machen. Also müssen wir es irgendwie transformieren.


Wir brauchen eine Funktion, die unsere Klasse übernimmt, jede Methode durchläuft, Metadaten mit Aktionstypen sammelt, eine Reduzierungskarte erstellt und aus dieser Reduzierungskarte einen endgültigen Reduzierer erstellt.


Hier erfahren Sie, wie Sie jede Methode einer Klasse untersuchen können.


 const getReducerClassMethodsWthActionTypes = (instance) => { // Get method names from class' prototype const proto = Object.getPrototypeOf(instance) const methodNames = Object.getOwnPropertyNames(proto).filter( (name) => name !== 'constructor', ) // We want to get back a collection with action types and corresponding reducers const res = [] methodNames.forEach((methodName) => { const actionTypes = Reflect.getMetadata( METADATA_KEY_ACTION, instance, methodName, ) // We want to bind each method to class' instance not to lose `this` context const method = instance[methodName].bind(instance) // We might have many action types associated with a reducer actionTypes.forEach((actionType) => res.push({ actionType, method, }), ) }) return res } 

Jetzt wollen wir die empfangene Sammlung in eine Reduzierungskarte verarbeiten.


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

Die endgültige Funktion könnte also ungefähr 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 } 

Und wir könnten es so auf unsere ReducerJedi Klasse anwenden.


 const reducerJedi = createClassReducer(ReducerJedi) 

Schritt 3. Alles zusammenführen.


 // We move that generic code to a dedicated module 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 { // Take a look at "Class field delcaratrions" proposal, which is now at 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) 

Nächste Schritte


Folgendes haben wir verpasst:


  • Was ist, wenn dieselbe Aktion mehreren Methoden entspricht? Die aktuelle Logik behandelt dies nicht.
  • Können wir immer hinzufügen?
  • Was ist, wenn ich klassenbasierte Aktionen verwende? Wie kann ich einen Aktionsersteller übergeben, keinen Aktionstyp?

All dies mit zusätzlichen Codebeispielen und Beispielen wird in der Reduziererklasse behandelt .


Ich muss sagen, dass die Verwendung von Klassen für Reduzierungen kein origineller Gedanke ist. @amcdnl hat vor einiger Zeit großartige ngrx-Aktionen entwickelt, aber es scheint, als ob er sich jetzt auf NGXS konzentriert, ganz zu schweigen davon, dass ich eine strengere Eingabe und Entkopplung von der Angular-spezifischen Logik wollte. Hier ist eine Liste der wichtigsten Unterschiede zwischen Reducer-Class- und Ngrx-Aktionen.


Wenn Ihnen die Idee gefällt, Klassen für Ihre Reduzierungen zu verwenden, möchten Sie dies möglicherweise auch für Ihre Aktionsersteller tun. Schauen Sie sich die Flux-Action-Klasse an .

Hoffentlich haben Sie etwas Nützliches für Ihr Projekt gefunden. Zögern Sie nicht, mir Ihr Feedback mitzuteilen! Ich freue mich über Kritik und Fragen.

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


All Articles