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

Modular Architecture inspired by Actors & Event Sourcing

Bob Dahlberg
December 14, 2020

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.

Bob Dahlberg

December 14, 2020
Tweet

More Decks by Bob Dahlberg

Other Decks in Programming

Transcript

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

    Stores Repo Data Business Domain UseCase Services Workers Tasks Pipes Manager Container
  2. "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/ </>
  3. Actor - one minute lecture actor<Message> { private var state:

    UserState } 1. An actor has a private mutable state. Rules of an actor
  4. 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
  5. 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
  6. The stream/broker/bus object AppStream { private val appScope = CoroutineScope(EmptyCoroutineContext

    SupervisorJob()) private val eventStream = MutableSharedFlow<Event>(extraBufferCapacity = 100) }
  7. 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) } } }
  8. 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 }
  9. 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() }
  10. 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
  11. 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)))
  12. 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, ...)))
  13. 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
  14. >> 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
  15. >> 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
  16. >> 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
  17. 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
  18. class FirebaseActor(private val logger: FirebaseCrashlytics) : Actor() { override suspend

    fun act(event: Event) { logger.log(event.toLogString()) } } Troubleshooting - FirebaseActor
  19. 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:
  20. 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:
  21. 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) } }
  22. 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()
  23. 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()