Composable Arsitektur

Muhammad Alfiansyah
4 min readOct 3, 2022

--

Photo by Sigmund on Unsplash

Your chapter in my book is over, It will forever be one of my favorites but I cant keep rereading it hoping for different ending :) ~ Thank you 🙏

Pada composable arsitektur tidak bisa lepas dari Reducer, Action dan State. Untuk masuk lebih dalam ke composable arsitektur kita perlu tau apa itu Reducer, Action dan State.

Reducer

Reducers are functions that take the current state and an action as arguments, and return a new state result. In other words, (state, action) => newState. (Redux)

Inti-nya Reducer berfungsi untuk mengubah current input (state dan action) menjadi state baru.

Action

Actions are plain JavaScript objects that have a type field. As mentioned earlier, you can think of an action as an event that describes something that happened in the application. (Redux)

Action mendiskripsikan apa yang terjadi pada aplikasi, bisa kita sebut juga user action, dimana user melakukan click, tap, hold, double tap etc.

State

One of the core principles of React and Redux is that your UI should be based on your state. So, one approach to designing an application is to first think of all the state needed to describe how the application works. It’s also a good idea to try to describe your UI with as few values in the state as possible, so there’s less data you need to keep track of and update. (Redux)

Jangan lupa juga

Store

The Redux store brings together the state, actions, and reducers that make up your app. The store has several responsibilities:

Holds the current application state inside

Allows access to the current state via store.getState();

Allows state to be updated via store.dispatch(action);

Registers listener callbacks via store.subscribe(listener);

Handles unregistering of listeners via the unsubscribe function returned by store.subscribe(listener).

It’s important to note that you’ll only have a single store in a Redux application. When you want to split your data handling logic, you’ll use reducer composition and create multiple reducers that can be combined together, instead of creating separate stores.

class Store<S, A> {
private let reducer: Reducer<S, A>
private var subscribers: [(S) -> Void] = []
private var currentState: S {
didSet {
self.subscribers.forEach { $0(self.currentState) }
}
}
init(reducer: Reducer<S, A>, inititalState: S) {
self.reducer = reducer
self.currentState = inititalState
}
func dispatch(_ action: A) {
self.reducer.reduce(&self.currentState, action)
}
func subscribe(_ subscriber: @escaping (S) -> Void) {
self.subscribers.append(subscriber)
subscriber(self.currentState)
}
}

Prisip Composable arsitektur adalah UI harus berdasarkan state. Jadi untuk mendesain app pertama kita harus berpikir state apa saja yang ada dan dibutuhkan untuk mendeskripsikan bagaimana aplikasi bekerja.

Masuk ke code, jadi Reducer meng-implement Monoid atau Monads. Jadi Reducer bersifat asosiatif. Sesuai dengan materi sebelumnya Reducer bisa kita combine dengan reducer lain sehingga menjadi satu Reducer baru.

struct Reducer<S, A> {
let reduce: (inout S, A) -> Void
}
extension Reducer: Monoid {
static var e: Reducer<S, A> {
return Reducer { s, _ in return }
}
static func <> (lhs: Reducer<S, A>, rhs: Reducer<S, A>) -> Reducer<S, A> {
return Reducer { s, a in
lhs.reduce(&s, a)
rhs.reduce(&s, a)
}
}
}

Untuk menggabungkan dua Reducer yang memiliki State dan Action yang berbeda kita harus menggunakan pendekatan baru, yaitu menggunakan function Lift untuk menyamakan mereka. Contoh kita memiliki accountReducer

let accountReducer = Reducer<AccountState, AccountAction> { state, action in
switch action {
case let .login(user):
state.logedInUser = user
case let .tappedEpisodes(episode):
state.watchedEpisodes.append(episode)
case .tappedLogout:
state.logedInUser = nil
case .tappedNotification(on: let on):
state.settings.notificationOn = on
}
}

dan EpisodeReducer seperti berikut

let episodeReducer = Reducer<EpisodesState, EpisodeAction> { state, action in
switch action {
case let .tappedEpisodes(episode):
state.watchedEpisodes.append(episode)
}
}

kita tidak bisa menggabungkan nya secara langsung karena

Reducer<AccountState, AccountAction> != Reducer<EpisodesState, EpisodeAction>

Jadi kita harus membuat Translator sehingga kedua Reducer tetap bisa berkomunikasi satu sama lain dengan cara kita haru melifting kedua Action dari Reducer menjadi AppAction dan State menjadi AppState,

struct AppState {
var episodeState: EpisodesState = .init()
var accountState: AccountState = .init()
}
enum AppAction {
case accountAction(AccountAction)
case episodeAction(EpisodeAction)
}

Jadi bagaimana proses lifting nya

extension Reducer {
func lift<T, B>(state: WritableKeyPath<T, S>, action: Prism<B,A>) -> Reducer<T, B> {
return Reducer<T, B> { t, b in
guard let a = action.tryGet(b) else { return }
self.reduce(&t[keyPath: state], a)
}
}
}

Sehingga resultnya akan menjadi

let liftedEpisodeReducer = episodeReducer.lift(state: \AppState.episodeState, action: AppAction.prism.episodeAction)let liftedAccountReducer = accountReducer.lift(state: \AppState.accountState, action: AppAction.prism.accountAction)

Dimana liftedEpisodeReducer dan liftedAccountReducer adalah Reducer<AppState, AppAction> sehingga kita bisa mengcombine keduanya, menjadi seperti berikut

let appReducer: Reducer<Appstate, AppAction> = liftedEpisodeReducer <> liftedAccountReducer

Untuk Full Source bisa di check dibawah.

Its better if we know what is going on on our code than we just use other person code without knowing what is behind

Jadi usahakan kita tau secara mengakar (secara Radikal) tentang apa yang kita kerjakan sehingga kita bisa dengan mudah mencari sumber kesalahan pada kode kita.

--

--