Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Modular Architecture inspired by Actors & Event Sourcing

Modular Architecture inspired by Actors & Event Sourcing

Building an app truly modular is hard. Lending ideas from actors and event sourcing is one approach I've found successful.
I've used it for several applications over the last couple of years, started with Java & Rx.
With Kotlin coroutines, channels, and flows we can now even omit Rx or other external libraries to make it happen.

0297b9b4bfd45c0f9c6c52bf696b7735?s=128

Bob Dahlberg

December 14, 2020
Tweet

Transcript

  1. Bob Dahlberg Author 2020-12-14 Date Modular Architecture inspired by Actors

    & Event Sourcing
  2. Background ANDROID SINCE CUPCAKE </>

  3. Android Architecture BEST PRACTICES </>

  4. MVVM

  5. View ViewModel LiveData MVVM

  6. The M Business Logic </>

  7. The M Business Logic </> Models Modules Beans Repositories Components

    Stores Repo Data Business Domain UseCase Services Workers Tasks Pipes Manager Container
  8. Actor the model </>

  9. "An actor is the primitive unit of computation. It’s the

    thing that receives a message and do some kind of computation based on it." https://www.brianstorti.com/the-actor-model/ </>
  10. Actor - one minute lecture actor<Message> { private var state:

    UserState } 1. An actor has a private mutable state. Rules of an actor
  11. Actor - one minute lecture actor<Message> { private var state:

    UserState for (msg in channel) { when(msg) { } } } 1. An actor has a private mutable state. 2. An actor has a mailbox where others can send messages to it. Rules of an actor
  12. Actor - one minute lecture actor<Message> { private var state:

    UserState for (msg in channel) { when(msg) { } } } 1. An actor has a private mutable state. 2. An actor has a mailbox where others can send messages to it. 3. An actor can perform one of three things when receiving a message • Create more actors • Send messages to other actors • Mutate its state (designate what to do with the next message) Rules of an actor
  13. The setup a reactive approach </>

  14. Rx or Flow? Is it different </>

  15. Why not a DI framework </>

  16. The stream/broker/bus object AppStream { private val appScope = CoroutineScope(EmptyCoroutineContext

    SupervisorJob()) private val eventStream = MutableSharedFlow<Event>(extraBufferCapacity = 100) }
  17. The stream/broker/bus object AppStream { private val appScope = CoroutineScope(EmptyCoroutineContext

    SupervisorJob()) private val eventStream = MutableSharedFlow<Event>(extraBufferCapacity = 100) fun send(event: Event) { appScope.launch { eventStream.emit(event) } } }
  18. The stream/broker/bus object AppStream { private val appScope = CoroutineScope(EmptyCoroutineContext

    SupervisorJob()) private val eventStream = MutableSharedFlow<Event>(extraBufferCapacity = 100) fun send(event: Event) { appScope.launch { eventStream.emit(event) } } val events: Flow<Event> = eventStream }
  19. The stream/broker/bus object AppStream { private val appScope = CoroutineScope(EmptyCoroutineContext

    SupervisorJob()) private val eventStream = MutableSharedFlow<Event>(extraBufferCapacity = 100) fun send(event: Event) { appScope.launch { eventStream.emit(event) } } val events: Flow<Event> = eventStream val states: Flow<State> = eventStream.filterIsInstance() }
  20. The application ow </>

  21. Fragment User clicks login in the UI and the Fragment

    calls its ViewModel to act. ViewModel Gets called to login. Transforms the input into a Message and sends it on the App Stream AppStream Broadcasts the Message to anyone who wants to receive it UserActor Listens to LoginMessages and performs an action to fullfil its intent. basic flow - login example Fragment Observes LiveData on the ViewModel for changes and updates the UI accordingly. ViewModel Listens to the new state and emits it on a LiveData object. AppStream Broadcasts the Message to anyone who wants to receive it UserActor When the private state changes, or fails to change, send a Message with the new state to the AppStream
  22. Fragment ViewModel AppStream UserActor basic flow - login example AuthActor

    API
  23. Fragment ViewModel AppStream UserActor basic flow - login example AuthActor

    API LogActor AnalyticsActor FirebaseActor
  24. Fragment ViewModel AppStream UserActor basic flow - login example AuthActor

    API LogActor AnalyticsActor FirebaseActor >> UserStatusEvent() << UserState(result=Ok(status=NO_USER, user=null)) >> LoginEvent(username=bob, password=***) << AuthState(result=Err(ConnectionFailure(message=Failed to connect to /192.168.1.100))) << UserState(result=Err(ConnectionFailure(message=Failed to connect to /192.168.1.100)))
  25. Fragment ViewModel AppStream UserActor basic flow - login example AuthActor

    API LogActor AnalyticsActor FirebaseActor >> UserStatusEvent() << UserState(result=Ok(status=NO_USER, user=null)) >> LoginEvent(username=bob, password=***) << AuthState(result=Ok(token=3a3ee647-fc3e-43c2-b5a6-f3b618fd9dab, user=User(name=bob, ...))) << UserState(result=Ok(status=ACTIVE, user=User(name=bob, ...)))
  26. Fragment ViewModel AppStream UserActor basic flow - login example AuthActor

    API LogActor AnalyticsActor FirebaseActor >> UserStatusEvent() << UserState(result=Ok(status=NO_USER, user=null)) >> LoginEvent(username=bob, password=***) << AuthState(result=Ok(token=3a3ee647-fc3e-43c2-b5a6-f3b618fd9dab, user=User(name=bob, ...))) >> WritePrefEvent(key=ACCESS_TOKEN, value=3a3ee647-fc3e-43c2-b5a6-f3b618fd9dab) << UserState(result=Ok(status=ACTIVE, user=User(name=bob, ...))) SharedPrefsActor
  27. Testing </>

  28. >> UserStatusEvent() << UserState(result=Ok(status=NO_USER, user=null)) >> LoginEvent(username=bob, password=***) << AuthState(result=Ok(token=3a3ee647-fc3e-43c2-b5a6-f3b618fd9dab,

    user=User(name=bob, ...))) >> WritePrefEvent(key=ACCESS_TOKEN, value=3a3ee647-fc3e-43c2-b5a6-f3b618fd9dab) << UserState(result=Ok(status=ACTIVE, user=User(name=bob, ...))) Testing one actor - UserActor UserActor().start() val resultList = async { AppStream.events .filters{ }.toList() } AppStream.send(UserStatusEvent()) AppStream.send(AuthState(Ok("fake"))) resultList.await() Validate all events collected
  29. >> UserStatusEvent() << UserState(result=Ok(status=NO_USER, user=null)) >> LoginEvent(username=bob, password=***) << AuthState(result=Ok(token=3a3ee647-fc3e-43c2-b5a6-f3b618fd9dab,

    user=User(name=bob, ...))) >> WritePrefEvent(key=ACCESS_TOKEN, value=3a3ee647-fc3e-43c2-b5a6-f3b618fd9dab) << UserState(result=Ok(status=ACTIVE, user=User(name=bob, ...))) UserActor().start() val resultList = async { AppStream.events .filters{ }.toList() } AppStream.send(UserStatusEvent()) AppStream.send(AuthState(Ok("fake"))) resultList.await() Validate all events collected Testing one actor - UserActor
  30. >> UserStatusEvent() << UserState(result=Ok(status=NO_USER, user=null)) >> LoginEvent(username=bob, password=***) << AuthState(result=Ok(token=3a3ee647-fc3e-43c2-b5a6-f3b618fd9dab,

    user=User(name=bob, ...))) >> WritePrefEvent(key=ACCESS_TOKEN, value=3a3ee647-fc3e-43c2-b5a6-f3b618fd9dab) << UserState(result=Ok(status=ACTIVE, user=User(name=bob, ...))) UserActor().start() AuthActor(api= mock).start() val resultList = async { AppStream.events .filters{ }.toList() } AppStream.send(UserStatusEvent()) AppStream.send(LoginEvent("bob", " ")) resultList.await() Validate all events collected Testing multiple actors - UserActor + AuthActor
  31. Troubleshooting </>

  32. class FakeStateActor() : Actor() { var started = false override

    suspend fun act(event: Event) { if(started event is CheckStatusEvent) { started = true send(LoginEvent("", "")) await<UserState>() send(PushEvent(bundleOf())) } } private suspend inline fun <reified T Event> await() { AppStream.events.filterIsInstance<T>().first() } } Troubleshooting - FakeStateActor
  33. class FirebaseActor(private val logger: FirebaseCrashlytics) : Actor() { override suspend

    fun act(event: Event) { logger.log(event.toLogString()) } } Troubleshooting - FirebaseActor
  34. class FirebaseActor(private val logger: FirebaseCrashlytics) : Actor() { override suspend

    fun act(event: Event) { logger.log(event.toLogString()) } } Troubleshooting - FirebaseActor 1. Get the users consent to collect anonymous data Remember to:
  35. class FirebaseActor(private val logger: FirebaseCrashlytics) : Actor() { override suspend

    fun act(event: Event) { logger.log(event.toLogString()) } } Troubleshooting - FirebaseActor 1. Get the users consent to collect anonymous data 2. Don't log sensitive or personal data -> toLogString() Remember to:
  36. Simplifying reads </>

  37. The AwaitEvent open class AwaitEvent<T>( private val deferred: CompletableDeferred<T> =

    CompletableDeferred() ) : Event() { suspend fun await() T = deferred.await() fun complete(value:T) { deferred.complete(value) } }
  38. The AwaitEvent - in actor class UserReadEvent : AwaitEvent<UserState>() class

    UserActor : Actor() { private var state: UserState override suspend fun act(event: Event) { when(event) { is UserReadEvent event.complete(state) is } } } val event = UserReadEvent() AppStream.send(event) val userState = event.await()
  39. The AwaitEvent - extensions class UserReadEvent : AwaitEvent<UserState>() class UserActor

    : Actor() { private var state: UserState override suspend fun act(event: Event) { when(event) { is UserReadEvent event.complete(state) is } } } val userState = UserReadEvent().send().await()
  40. Baby steps? Room </>

  41. View ViewModel Room Actors AppStream

  42. Bob Dahlberg Tech Lead Mobile See more twitter.com/mr_bob speakerdeck.com/bobdahlberg bob-dahlberg.medium.com

    Thank you bob@qvik.com