Organisasi peredam - melangkah lebih jauh


Apa yang akan kita bahas di sini?


Kita akan meninjau evolusi reduksi di aplikasi Redux / NGRX saya yang terjadi selama dua tahun terakhir. Mulai dari vanilla switch-case , pergi ke memilih peredam dari objek dengan kunci, akhirnya diselesaikan dengan reduksi berbasis kelas. Kami tidak hanya akan berbicara tentang bagaimana, tetapi juga tentang mengapa.


Jika Anda tertarik untuk bekerja di sekitar terlalu banyak boilerplate di Redux / NGRX Anda mungkin ingin memeriksa artikel ini .

Jika Anda sudah terbiasa memilih peredam dari teknik peta, pertimbangkan untuk langsung beralih ke peredam berbasis kelas .

Kasing sakelar vanila


Jadi mari kita lihat tugas sehari-hari untuk membuat entitas di server secara tidak sinkron. Kali ini saya sarankan kita menjelaskan bagaimana kita bisa membuat jedi baru.


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

Biarkan saya jujur, saya tidak pernah menggunakan reduksi semacam ini dalam produksi. Alasan saya ada tiga:


  • switch-case memperkenalkan beberapa titik ketegangan, pipa bocor, yang kita mungkin lupa untuk menambal waktu di beberapa titik. Kita selalu bisa lupa untuk break jika tidak segera return , kita selalu bisa lupa untuk menambahkan default , yang harus kita tambahkan ke setiap peredam.
  • switch-case memiliki beberapa kode boilerplate sendiri yang tidak menambahkan konteks apa pun.
  • switch-case adalah O (n), semacam . Ini bukan argumen yang kuat dengan sendirinya karena Redux tidak terlalu performant, tapi itu membuat perfeksionis batin saya marah.

Langkah logis berikutnya yang disarankan oleh dokumentasi resmi Redux adalah mengambil peredam dari suatu objek dengan kunci.


Memilih peredam dari objek dengan kunci


Idenya sederhana. Setiap transformasi keadaan adalah fungsi dari keadaan dan tindakan dan memiliki jenis tindakan yang sesuai. Mempertimbangkan bahwa setiap jenis tindakan adalah string, kita dapat membuat objek, di mana setiap kunci adalah jenis tindakan dan setiap nilai adalah fungsi yang mengubah keadaan (reducer). Lalu kita bisa memilih peredam yang diperlukan dari objek itu dengan kunci, yaitu O (1), ketika kita menerima tindakan baru.


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

Yang keren di sini adalah bahwa logika di dalam reducerJedi tetap sama untuk peredam apa pun, yang berarti kita dapat menggunakannya kembali. Bahkan ada perpustakaan kecil, yang disebut redux-create-reducer , yang melakukan hal itu. Itu membuat kode terlihat seperti ini:


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

Bagus dan cantik, ya? Meskipun cantik ini masih memiliki beberapa peringatan:


  • Dalam hal reduksi kompleks kita harus meninggalkan banyak komentar yang menjelaskan apa yang dilakukan peredam ini dan mengapa.
  • Peta peredam besar sulit dibaca.
  • Setiap peredam hanya memiliki satu jenis tindakan yang sesuai. Bagaimana jika saya ingin menjalankan peredam yang sama untuk beberapa tindakan?

Peredam berbasis kelas menjadi gudang cahaya saya di kerajaan malam itu.


Reduksi berbasis kelas


Kali ini saya akan mulai dengan mengapa pendekatan ini:


  • Metode kelas akan menjadi reduksi kami dan metode memiliki nama, yang merupakan informasi meta yang berguna, dan kami dapat meninggalkan komentar dalam 90% kasus.
  • Metode-metode kelas dapat didekorasi yang merupakan cara deklaratif yang mudah dibaca untuk mencocokkan aksi dan reduksi.
  • Kita masih bisa menggunakan peta tindakan di bawah tenda untuk memiliki kompleksitas O (1).

Jika itu terdengar seperti daftar alasan yang masuk akal untuk Anda, mari gali!


Pertama-tama, saya ingin mendefinisikan apa yang ingin kita dapatkan sebagai hasilnya.


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

Sekarang seperti yang kita lihat di mana kita ingin mendapatkan kita bisa melakukannya langkah demi langkah.


Langkah 1. Dekorator aksi .


Apa yang ingin kita lakukan di sini adalah menerima sejumlah jenis tindakan dan menyimpannya sebagai meta-informasi untuk metode kelas yang akan digunakan nanti. Untuk melakukan itu kita bisa menggunakan polyfill reflect-metadata , yang membawa fungsionalitas meta-data ke objek Reflect . Setelah itu dekorator ini hanya akan melampirkan argumennya (jenis tindakan) ke metode sebagai meta-data.


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

Langkah 2. Membuat fungsi peredam dari kelas peredam


Seperti yang kita ketahui, setiap peredam adalah fungsi murni yang menerima status dan aksi serta mengembalikan status baru. Nah, kelas adalah fungsi juga, tetapi kelas ES6 tidak dapat dipanggil tanpa yang new dan kita harus membuat peredam aktual dari kelas dengan beberapa metode. Jadi kita perlu mengubahnya.


Kita membutuhkan fungsi yang akan mengambil kelas kita, berjalan melalui setiap metode, mengumpulkan metadata dengan jenis tindakan, membangun peta peredam dan membuat peredam akhir dari peta peredam itu.


Inilah cara kami memeriksa setiap metode dalam suatu kelas.


 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 } 

Sekarang kami ingin memproses koleksi yang diterima menjadi peta peredam.


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

Jadi fungsi akhirnya bisa terlihat seperti ini.


 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 } 

Dan kita bisa menerapkannya ke kelas ReducerJedi kita seperti ini.


 const reducerJedi = createClassReducer(ReducerJedi) 

Langkah 3. Menggabungkan semuanya.


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

Langkah selanjutnya


Inilah yang kami lewatkan:


  • Bagaimana jika tindakan yang sama sesuai dengan beberapa metode? Logika saat ini tidak menangani ini.
  • Bisakah kita menambahkan immer ?
  • Bagaimana jika saya menggunakan tindakan berbasis kelas? Bagaimana saya bisa melewati pembuat tindakan, bukan jenis tindakan?

Semua itu dengan contoh kode tambahan dan contoh ditutupi dengan kelas peredam .


Saya harus mengatakan bahwa menggunakan kelas untuk reduksi bukanlah pemikiran asli. @amcdnl muncul dengan aksi ngrx yang mengagumkan beberapa waktu yang lalu, tapi sepertinya dia sekarang fokus pada NGXS , belum lagi saya ingin mengetik dan decoupling yang lebih ketat dari logika khusus angular. Berikut adalah daftar perbedaan utama antara tindakan peredam dan tindakan ngrx.


Jika Anda menyukai gagasan untuk menggunakan kelas untuk reduksi Anda, Anda mungkin ingin melakukan hal yang sama untuk pembuat tindakan Anda. Lihatlah kelas aksi-fluks .

Semoga Anda menemukan sesuatu yang berguna untuk proyek Anda. Silakan sampaikan umpan balik Anda kepada saya! Saya sangat menghargai kritik dan pertanyaan apa pun.

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


All Articles