-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add CompositeStore
#3
Add CompositeStore
#3
Conversation
Codecov Report
@@ Coverage Diff @@
## master #3 +/- ##
============================================
- Coverage 76.42% 72.57% -3.85%
- Complexity 49 63 +14
============================================
Files 9 11 +2
Lines 263 361 +98
Branches 37 47 +10
============================================
+ Hits 201 262 +61
- Misses 47 77 +30
- Partials 15 22 +7
Continue to review full report at Codecov.
|
I started review and so far this looks very promising. I'm not quite done yet, but I wanna share my thoughts so far, to get a conversation started: TestsChewing through the tests I made a couple of adjustments, see alex2069#1
API surface & DocumentationI'd like to keep the exposed classes, objects, enums, functions and constants to a minimum. That'll allow us to freely change anything that's not public within a major version (except for behavior of course).
DesignOn the middleware topic: what middlewares do we tyically use? Thunk and...? I've come to the view that we should avoid middlewares altogether. Thunk is the only valid use case for middlewares that I'm aware off (outside of testing) and that one is actually not so valid after all. The store should not be the engine for asynchronous behavior, coroutines (or Rx or whatever) should be. And in reality they are! Thunks typically need a coroutine scope to do anything meaningful. class SomeAsyncLogicInAThunk(
private val scope: CoroutineScope,
private val repository: SomeRepository,
private val someId: UUID
) : () -> Thunk<AppState> {
override fun invoke() = thunk<AppState> { dispatch, _ ->
dispatch(StartLoadingAction)
scope.launch {
try {
val something = repository.loadSomethingById(someId)
dispatch(SomethingLoadedAction(something))
} catch (e: RepositoryException) {
dispatch(FaledToLoadSomethingAction)
}
}
}
}
// callsite
val someAsyncLogicInAThunk = // get via DI
store.dispatch(someAsyncLogicInAThunk) Instead we should just program async behavior in some logic object, let's say a class UseCase(
private val scope: CoroutineScope,
private val repository: SomeRepository,
private val dispatch: DispatchFunction
) {
fun someAsyncLogic(someId: UUID) {
dispatch(StartLoadingAction)
scope.launch {
try {
val something = repository.loadSomethingById(someId)
dispatch(SomethingLoadedAction(something))
} catch (e: RepositoryException) {
dispatch(FaledToLoadSomethingAction)
}
}
}
}
// callsite
val useCase = // get via DI
useCase.someAsyncLogic I believe Redux has nothing to do with running business logic. All it does is manage application state. If that holds then we should get rid of middlewares. But that's a larger discussion and we don't need to resolve that for this PR. However: if we only use the thunk middleware, we could just use it in all stores and don't need to add it to composite store, right? Because the composite store always delegates, so even if only one of the stores (say Also: If you have a valid usecase for middlewares - please let me know! |
So quick disclaimer to save a little face lol - I kind of forgot to finish cleaning this up as this was never originally intended to go into this (public) repo. Originally the plan was to write it up in this project (for easier testing/development) and just put it in the LinkApp repo. Anyway, all that aside, your changes to the test are fine with me (and TBH can be further refined if needed - again it was meant to be added to another repo so I tried not to reuse a lot of the pre-existing test architecture). API surface & Documentation
Done
We could - TBH I'm more inclined to just make them
I've held off on documentation until it was confirmed we'll use this, so once the API is confirmed then I'll add some for sure. MiddlewareAs for the middleware; I can't speak to the design aspect with authority as my experience is limited to only personal small web projects outside of LinkApp, but it's used in a couple of places at the moment which kind of make sense as middleware. NavigationIt's hard to judge whether there are better ways to do it as I didn't write the initial implementation (Majdi would know), but the navigation architecture (the router) uses it for push/pop/history/etc stuff;
RewardSDKThe RewardSDK declares a A nice aspect of this - if we want to remove the RewardSDK, you just remove the middleware and that's it - so in that regard it is good for encapsulating the logic. AnalyticsSimilarly to the RewardSDK, a lot of actions are also the analytics triggers. These are dispatch to Rakuten Analytics and/or Adjust Analytics etc. Single
|
review & expand unit tests
a couple of disconnected thoughts: about middlewares:
but all in all there are a few valid reasons to keep middlewares so.... let's keep middlewares (I guess) 🤷♀️ Regarding the thunks: can you create a unit test for parent/child store that fails with the type cast exception? I think once we merge this we can discard parent/child store and publish a new major version. another visibility concern: the I'll revisit the code sometime later this week to get some time to think about the implications of this design. |
Ok a few updates;
Ah yes, not really ideal on public API heh. I've reduced their visibility using |
Used in modularization PoC at https://github.com/Rakuten-MTSD-PAIS/mavenir-android-client-snapshot/pull/3059
Store
Store
(only the declaration of the stores changes)The behavior of the composite store ensures that all related stores are appropriately updated etc no matter which module triggers a store state change. The tests show the full relations/behavior between stores, but in summary;
Effect
dispatched toModuleStore
will trigger any listeners on theirModuleStore
or anyCompositeStore
that includesModuleStore
Effect
dispatched toCompositeStore
will dispatch to its included storesAction
dispatched to theModuleStore
will triggerCompositeStore
subscribersAction
dispatched to theCompositeStore
is dispatched to all included stores (and triggers subscribers in other composite stores appropriately as states change etc.)Because each module has its own store and state type, it can also use
thunk
middleware internally (not possible with child stores). So in the PoC above it allows having a "navigation" module that handles the router state. Its store can declare and use its own middleware to manipulate the navigation state without exposing its middleware or other internals to the rest of the app.