freeletics / RxRedux

Redux implementation based on RxJava

Home Page:https://freeletics.engineering/2018/08/16/rxredux.html

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to compose state machines?

ar-g opened this issue · comments

I have two almost identical screens in our app, but they differ in two side effects. I wrote separate presenter and view for another screen.

Now the problem is how do I reuse the state machine from a similar screen? This state machine consists of 15 different side-effects, and I don't want to duplicate this functionality. Currently, I'm polluting it with several if statements inside of side-effects and configurable input object for a state machine.

Hi,
can you give a more concrete example? Does the screen state differs as well?

Screen states are the same, although UI is a little bit different visually.

We have two types of chat-list screens. "AllChats" and "TriggerChats".

Logic is very similar but 2 side-effects differ in api/db calls, currently, I have some type variable to differentiate this behavior as an input param to a state machine. My concerns are that this screen will definitely evolve in the future and I'll need to duplicate much of functionalities.

If there would be a way to have small composable state machines and one bigger one it may work somehow. Have you come across similar issues?

Well I think in that case you can create an entire new state machine by reusing the same redcuer, state, action and side effect. So I would extract the reducer into a function and all side effects into a function as well.

sealed class Action { ... }
sealed class State { ... }

fun reducer(state : State, action : Action) { ... }

fun sideEffect1(actions : Observable<Action>, stateAccessor: StateAccessor<State>) : Observable<Action> { ... }

fun fooSideEffectWithHttp(actions : Observable<Action>, stateAccessor: StateAccessor<State>) : Observable<Action> = 
   action.ofType(Foo:Action:class.java)
              .switchMap { httpClient.getFoo().map { FooLoadedAction(it) } }

fun sideEffect3(actions : Observable<Action>, stateAccessor: StateAccessor<State>) : Observable<Action> { ... }

Then you can create two different statmachine as something like that:

// Statemachine 1
class StateMachine1 {
    val input : PublishSubject<Action> = ...

   val state = input
        .reduxStore(
             initialState = ... ,
             sideEffects = listOf(::sideEffect1, ::fooSideEffectWithHttp, ::sideEffect3),
             reducer= ::reducer
}

In your second state machine you want instead of sideEffect2 a different implementation (i.e. use database instead of making an http call) so create a new SideEffect that behaves from public API standpoint exactly the same as sideEffect2 does (reacts on same type of input action(s) and emit the same type of output action(s) ) like this

fun fooSideEffectWithDatabase(actions : Observable<Action>, stateAccessor: StateAccessor<State>)  : Observable<Action> = 
   action.ofType(Foo:Action:class.java)
              .switchMap { database.getAllFoo().map { FooLoadedAction(it) } }

// Statemachine 2
class StateMachine2 {
    val input : PublishSubject<Action> = ...

   val state = input
        .reduxStore(
             initialState = ... ,
             sideEffects = listOf(::sideEffect1, ::fooSideEffectWithDatabase, ::sideEffect3),
             reducer= ::reducer
}

That's how I would "compose" state machines in that case. Not by inheriting, but by reusing the same reducer and side effects (that's why side effects by design are functions, so that you can reuse them).

Does that help?

If not, you could also compose a statemachine from an entire other state machine by wraping the code State of a StateMachine into an Action something like this:

```kotlin
// Statemachine 1
class StateMachine1 {
    val input : PublishSubject<Action> = ...

   val state = input
        .reduxStore(
             initialState = ... ,
             sideEffects = listOf(::sideEffect1, ::sideEffect2, ::sideEffect3),
             reducer= ::reducer
}

and then:

// StateMachine 2
class StateMachine2( private val stateMachine1 : StateMachine1) {

 val input : PublishSubject<Action> = ...

   val state = Observable.merge(
     input, 
     stateMachine1.state.map { newState -> StateChangedAction(newState) }
   )
        .reduxStore(
             initialState = ... ,
             sideEffects = listOf(...),
             reducer=  ... // Reducer knows how to deal with StateChangedAction
}

Or you could do the wraping of state into action in a sideeffect if you like that better:

// StateMachine 2
class StateMachine2( private val stateMachine1 : StateMachine1) {

 val input : PublishSubject<Action> = ...

   val state = input
        .reduxStore(
             initialState = ... ,
             sideEffects = listOf(..., observingStateMachine1, forwardActionToStateMachine1, ... ),
             reducer=  ... // Reducer knows how to deal with StateChangedAction


  private fun observingStateMachine1( _ : Observable<Action>, _ : StateAccessor<State>) : Observable<Action> = 
          stateMachine1.state.map { newState -> StateChangedAction(newState) }

   // and in case you need to forward an action to stateMachine 1 this could work:
  private fun forwardActionToStateMachine1( actions : Observable<Action>, _ : StateAccessor<State>) : Observable<Action> = 
    actions.ofType(SomeSpecialAction::class.java)
            .switchMap { action ->
                      stateMachine1.input.onNext(action)
                      Observable.empty() // means no Action as output
               } 
}

As you see, it's composable in many ways:
Composable by reusing small building blocks like SideEffects and Reducer or by wraping the State of one entire state machine into an Action and use it as an Action / input for another state machine.

Hope that helps

Thank you for such a detailed answer!

I do use the first solution among some screens, the problem here is that I have to pass external dependencies to side-effect and if I have 15 of them this becomes messy, although reusing reducer is a neat trick.

For example this func I have to pass to the list of side effects which is adding some extra level of the ceremony but this becomes truly composable on the other side.

val func: (actions: Observable<Action>, state: StateAccessor<State>) -> Observable<Action> = {
        actions, state -> sendMsgClickSideEffect(actions, state, chat, chatAnalytics, resStorage, messageOperations)
    }

val state: Observable<State> by unsafeLazy {
        input.reduxStore(
                initialState = initialState,
                sideEffects = listOf(
                    func//or wrapper func
                ),
                reducer = ::reducer
            )

The second idea is interesting and allows to have dependencies hidden, but requires a very good understanding of logic to have StateMachines well divided.

It seems that there is no way to avoid dependencies scattered and duplicated if I want it to be composable.

To create side effects with dependencies - you can always wrap side effect into some class and use DI (like dagger) to provide them into state machine:

    class SideEffectWithDeps @Inject constructor(
        private val someDep: SomeDep
    ) : SideEffect<String, Int> {
        override fun invoke(actions: Observable<Int>, state: StateAccessor<String>): Observable<out Int> {
            return actions.switchMap { someDep.calculate(it) }
        }
    }

    class StateMachine @Inject constructor(
        seWithDeps: SideEffectWithDeps
    ) {
        val input = PublishSubject.create<Int>()
        val store = input
            .reduxStore(
                initialState = "", 
                sideEffects = listOf(seWithDeps)
            ) { currentState, _-> currentState }
    }

Advanced use-case would be having a module per state machine that assembles all side effects into list and this list is provided to state machine constructor:

    class StateMachine @Inject constructor(
        @StateMachineSE sideEffects: List<SideEffect<String, Int>>
    ) {
        val input = PublishSubject.create<Int>()
        val store = input
            .reduxStore(
                initialState = "",
                sideEffects = sideEffects
            ) { currentState, _-> currentState }
    }

This one is pretty cool and looks very composable, I didn't think of it. Thanks, @Tapchicoma, I will definitely try this approach.