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

A Journey Into Reactive Systems

A Journey Into Reactive Systems

Kristijan Rebernisak

January 16, 2017
Tweet

More Decks by Kristijan Rebernisak

Other Decks in Technology

Transcript

  1. • Reactive data layer • Reactive features • Functional components

    • Managing state • Event sourcing • Reactive Data Flow
  2. SqlBrite sqlBrite = SqlBrite.create(); // wraps SQLiteOpenHelper
 BriteDatabase db =

    sqlBrite.wrapDatabaseHelper(openHelper, Schedulers.io());
 
 // wraps ContentResolver
 BriteContentResolver resolver = 
 sqlBrite.wrapContentProvider(contentResolver, Schedulers.io());
  3. // create query from database
 QueryObservable query = db.createQuery(table, sqlQuery,

    args);
 // or from content resolver
 query = resolver.createQuery(uri, projection, selection, /*...*/); // map to a Type
 query.mapToOne(cursor -> /*...*/). // Observable(T)
 
 query.mapToOneOrDefault(cursor -> /*...*/, defaultItem). // Observable(T)
 
 query.mapToList(cursor -> /*...*/). // Observable(List<T>)
  4. "We were always playing catch-up to try and integrate all

    of our separate tools together. Firebase just replaced most of these services, and everything was already streamlined for us."— Amine Laadhari, CTO/Co-Founder "Firebase was an obvious solution to our challenges. We were confident that we could rely on Google’s scale to accurately measure both user behavior and critical errors." — Iordanis Giannakakis, Lead Android Developer "We trust Firebase to provide a reliable and scalable mobile platform to support our business needs. Firebase was easy to implement, which allowed us to pick up benefits from multiple services in just a few days." — Bálint Orosz, Head of App Product
  5. public static Observable<DataSnapshot> observe(Query ref) {
 return Observable.fromEmitter(
 emitter ->

    {
 ValueEventListener listener =
 ref.addValueEventListener(
 new ValueEventListener() {
 @Override
 public void onDataChange(DataSnapshot snapshot) {
 emitter.onNext(snapshot);
 }
 
 @Override
 public void onCancelled(DatabaseError error) {
 // Map DatabaseError into a throwable to conform to the API
 emitter.onError(RxFirebaseException.from(error));
 }
 });
 
 // When the subscription is cancelled, remove the listener
 emitter.setCancellation(() -> ref.removeEventListener(listener));
 },
 BackpressureMode.BUFFER);
 }
  6. public static Single<DataSnapshot> once(Query ref) {
 return Single.create(
 subscriber ->

    {
 ValueEventListener listener =
 ref.addListenerForSingleValueEvent(
 new ValueEventListener() {
 // ... do the same as for addValueEventListener
 });
 
 // When the subscription is cancelled, remove the listener
 subscriber.add(Subscriptions.create(() -> ref.removeEventListener(listener)));
 });
 }
  7. public static Observable<ChildEvent> observeChildren(Query ref) {
 return Observable.fromEmitter(
 emitter ->

    {
 ChildEventListener listener =
 ref.addChildEventListener(
 new ChildEventListener() {
 @Override
 public void onChildAdded(DataSnapshot snapshot, String prevName) {
 emitter.onNext(new ChildEvent(snapshot, EventType.CHILD_ADDED, prevName));
 }
 
 // ... CHILD_CHANGED, CHILD_REMOVED, CHILD_MOVED
 
 @Override
 public void onCancelled(DatabaseError error) {
 emitter.onError(RxFirebaseException.from(error));
 }
 });
 
 // When the subscription is cancelled, remove the listener
 emitter.setCancellation(() -> ref.removeEventListener(listener));
 },
 BackpressureMode.BUFFER);
 }
  8. Meditation - feature overview • Complete exercise • Set personal

    goals • See detailed data for day • See overview for week
  9. Meditation - architecture • 3 layer architecture • Data layer

    - simple save and load (Firebase) • Domain layer - business logic • UI layer - presentation to user • Rx - glue that holds everything together
  10. static func value(query: FIRDatabaseQuery) -> Observable<FIRDataSnapshot> { } return Observable.create

    { observer in let handle = query.observe(.value, with: { snapshot in observer.onNext(snapshot) }, withCancel: { error in observer.onError(error) }) return Disposables.create { query.removeObserver(withHandle: handle) } } struct RxFirebase { } Firebase - value event
  11. struct FirebaseMeditationGoalRepository: MeditationGoalRepository { func lastGoal(before: Date, type: MeditationGoalType, userId:

    String) -> Observable<MeditationGoal> { } } struct MeditationGoal { var time: Date let type: MeditationGoalType var value: Int } let path = DbReferences.Meditation.goals(type: type, userId: userId) let filter = NSNumber(value: time.timeIntervalSince1970) let query = self.root .child(path) .queryOrdered(byChild: "time") .queryEnding(atValue: filter) .queryLimited(toLast: 1) return RxFirebase .value(query: query) .map { MeditationGoal(snapshot: $0) } enum MeditationGoalType { case duration case sessions } protocol MeditationGoalRepository { func save(goal: MeditationGoal) -> Observable<MeditationGoal> func lastGoal(before: Date, type: MeditationGoalType, userId: String) -> Observable<MeditationGoal> } Meditation - data layer
  12. struct MeditationSession { var start: Date var end: Date let

    exercise: MeditationExercise let flavour: MeditationFlavour var score: MeditationScore } protocol MeditationSessionRepository { func save(session: MeditationSession) -> Observable<MeditationSession> func sessions(userId: String, startDate: Date, endDate: Date) -> Observable<[MeditationSession]> } Meditation - data layer
  13. protocol MeditationService { func activeGoal(userId: String, time: Date) -> Observable<MeditationGoal>

    func sessions(userId: String, date: Date) -> Observable<[MeditationSession]> func day(userId: String, date: Date) -> Observable<MeditationDayModel> func week(userId: String, anyDateInWeek date: Date) -> Observable<[MeditationDayModel]> } Meditation - domain layer
  14. struct DefaultMeditationService: MeditationService { } Meditation - domain layer func

    activeGoal(userId: String, time: Date) -> Observable<MeditationGoal> { let durationGoal = self .goalRepository .lastGoal(before: time.endOfLocalDay, type: .duration, userId: userId) let sessionsGoal = self .goalRepository .lastGoal(before: time.endOfLocalDay, type: .sessions, userId: userId) return Observable.combineLatest(durationGoal, sessionsGoal) { $0.time >= $1.time ? $0 : $1 } } func sessions(userId: String, date: Date) -> Observable<[MeditationSession]> { let startDate = date.beginningOfLocalDay let endDate = date.endOfLocalDay return self .sessionRepository .sessions(userId: userId, startDate: startDate, endDate: endDate) }
  15. struct DefaultMeditationService: MeditationService { } Meditation - business layer func

    day(userId: String, date: Date) -> Observable<MeditationDayModel> { let exercises = self.sessions(userId: userId, date: date) let goal = self.activeGoal(userId: userId, time: date) return Observable.combineLatest(exercises, goal) { MeditationDayModel(date: date, sessions: $0, goal: $1) } } func week(userId: String, anyDateInWeek date: Date) -> Observable<[MeditationDayModel]> { return Observable.combineLatest( weekDates(anyDateInWeek: date) .map { self.day(userId: userId, date: $0) } ) { $0 } } func weekDates(anyDateInWeek: Date) -> [Date]
  16. class MeditationViewController: UIViewController { let service: MeditationService override func viewDidLoad()

    { super.viewDidLoad() } } Meditation - UI layer self.service .day(userId: "1234", date: Date()) .subscribe( ) onNext: { viewModel in // apply view model to views }, onError: { error in // report error }
  17. Issues • View events are not streamed • View updates

    are imperative • Presenter contains some logic to combine data streams and update the view • Presenter has a lifecycle • Stream error handling • Multiple presenters and communication between them • Managing state
  18. This complexity is difficult to handle as we're mixing two

    concepts that are very hard for the human mind to reason about: mutation and asynchronicity.
  19. • UIs are cycles. • UIs are functions. • UIs

    are asynchronous. • UIs are symmetric. • The User is a function.
  20. Event • Events happen in the past. • Events are

    immutable. • Events are one-way messages. • Events carry data. • Events should describe business intent.
  21. Event sourcing is a way of persisting your application's state

    by storing the history that determines the current state of your application.
  22. • Performance • Simplification • Audit trail • Integrations •

    Not loosing data • Deriving additional business value from the event history. • Production troubleshooting. Fixing errors. • Testing
  23. • Complexity • Event Sourcing & Command Sourcing confusion •

    Side-effects on event replay • Performance • Local and cloud storage size • Eventual consistency • Evolving events
  24. Following in the steps of Flux, CQRS, and Event Sourcing,

    Redux attempts to make state mutations predictable by imposing certain restrictions on how and when updates can happen. These restrictions are reflected in the three principles of Redux.
  25. Three Principles • Single source of truth - The state

    of your whole application is stored in an object tree within a single store. • State is read-only - The only way to change the state is to emit an action, an object describing what happened. • Changes are made with pure functions - To specify how the state tree is transformed by actions, you write pure reducers.
  26. @AutoValue
 abstract class Event {
 
 interface View {
 Event

    raw();
 }
 
 abstract String id();
 abstract String type();
 abstract long timestamp();
 abstract String origin();
 abstract Map<String, Object> data();
 
 public static Builder builder() {
 return new AutoValue_Event.Builder()
 .id(UUID.randomUUID().toString())
 .timestamp(System.currentTimeMillis());
 }
 
 // ...
 }
  27. /** Could go through different pipes */
 static List<String> domainTypes

    = Arrays.asList("text-message", "subscription");
 
 static List<String> infrastructureTypes =
 Arrays.asList("restart", “ping”, "text-message-producer-error");
  28. @AutoValue
 abstract class MessageSubscription implements Event.View {
 
 abstract String

    text();
 abstract int frequency();
 abstract boolean enabled();
 
 String key = () -> raw().type() + ":" + raw().origin();
 
 // ...
 }
 
 @AutoValue
 abstract class TextMessage implements Event.View {
 
 public static final String TYPE_DIRECT = "text-message:direct";
 public static final String TYPE_GROUP = "text-message:group";
 
 abstract String destination();
 abstract String body(); 
 
 // ...
 }
  29. @AutoValue
 abstract class Restart implements Event.View {
 
 abstract String

    target();
 
 static boolean isOf(Event event, String type) {
 return event.type().equals("restart") 
 && Restart.from(event).target().equals(type);
 }
 
 // ...
 }
 
 static Event ping(String origin) {
 return Event.builder()
 .type("ping")
 .origin(origin)
 .data(Collections.emptyMap())
 .build();
 }
  30. /**
 * Event Sourcing pub/sub example, with multiple flaky services


    * producing messages and clients consuming messages
 */
 public class BellabeatReduxDemo extends Activity {
 
 private static final String[] SERVICES = {
 MessageProducer.TYPE,
 Subscriptions.TYPE,
 Gamification.TYPE,
 MarketingService.TYPE,
 Inbox.TYPE,
 ServiceAnalytics.TYPE
 };
 
 // ...
 }
  31. @Override
 protected void onStart() {
 super.onStart();
 initView();
 
 ReplaySubject<Event> source

    = ReplaySubject.create();
 subscription.addAll(bindServices(source), bindView(source));
 }
 
 @Override
 protected void onStop() {
 super.onStop();
 subscription.clear();
 }
 
 // ...
  32. private CompositeSubscription bindServices(ReplaySubject<Event> source) {
 Subscriptions.Output output = 
 Subscriptions.dbFrom(Subscriptions.Db.empty(),

    source);
 Subscription s1 = output.events().subscribe(source);
 
 Observable<Subscriptions.Db> db = output.db().share();
 
 Subscription s2 =
 Observable.merge(
 MessageProducer.from(source),
 Gamification.from(source, db),
 MarketingService.from(source, db))
 .subscribe(source);
 
 return new CompositeSubscription(s1, s2);
 }
 
 // ...
  33. private Subscription bindView(ReplaySubject<Event> source) {
 return new CompositeSubscription(
 bindEventLog(source), bindSubscribers(source),

    bindControllers(source));
 } private Subscription bindEventLog(
 ArrayAdapter<Event> adapter, ReplaySubject<Event> source) {
 return source
 .observeOn(AndroidSchedulers.mainThread())
 .subscribe(
 event -> {
 adapter.add(event);
 adapter.notifyDataSetChanged();
 });
 }
 
 private Subscription bindChaosMonkey(View view, ReplaySubject<Event> source) {
 return RxView.clicks(view).map(e -> randomRestart()).subscribe(source);
 }
 
 // ...
  34. /** Messaging service which reacts to restart events, and outputs

    messages to clients */
 class MessageProducer {
 
 static final String TYPE = "message_producer";
 
 Observable<Event> from(Observable<Event> source) {
 return source
 .filter(event -> Restart.isOf(event, MessageProducer.TYPE))
 .switchMap(
 event ->
 Observable.just(Types.ping(MessageProducer.TYPE))
 .concatWith(messagingDriver(link(source)))
 .delaySubscription(3, TimeUnit.SECONDS));
 }
 
 private Observable<MessageSubscription> link(Observable<Event> source) {
 return source
 .filter(event -> event.type().equals("subscription"))
 .map(MessageSubscription::from);
 }
 
 // ...
 }
  35. private Observable<Event> messagingDriver(Observable<MessageSubscription> subscriptions) {
 return subscriptions
 .groupBy(MessageSubscription::key, ms ->

    ms)
 .flatMap(group -> group.switchMap(MessageProducer::toMessageStream))
 .onErrorReturn(MessageProducer::errorMessageLog); // TODO: add watchdog
 }
 
 /** Message stream for a specific subscription. Stream is flaky and can error sometimes. */
 private Observable<Event> toMessageStream(MessageSubscription subscription) {
 if (!subscription.enabled()) return Observable.empty();
 return Observable.interval(subscription.frequency(), TimeUnit.SECONDS)
 .map(i -> calculateMessageText(subscription))
 .map(text -> TextMessage.direct(MessageProducer.TYPE, subscription.raw().origin(), text))
 .map(Event.View::raw);
 }
 
 /** Intensive message body calculation with some side-effects, an occasional error */
 private String calculateMessageText(MessageSubscription subscription)
 throws SubscriptionProcessingException { /* */ }
 
 // ...
  36. public class Subscriber {
 
 @AutoValue
 abstract static class ViewState

    {
 abstract String command();
 abstract boolean valid();
 abstract String message();
 // ...
 }
 
 interface Output {
 Observable<ViewState> state();
 Observable<Event> events();
 }
 
 interface Input {
 ViewState initState();
 String origin();
 Observable<String> inputChanges();
 Observable<String> keyEnterEvents();
 Observable<String> messages();
 }
 
 static Output model(Input input) {
 return new Output() { /* Data Flow */ };
 }
 
 // ...
 }
  37. /** Database service that contains all messages grouped by destination

    */
 class Inbox {
 
 static final String TYPE = "inbox";
 
 @AutoValue
 abstract static class Db {
 
 abstract Map<String, List<String>> messages();
 
 String selectLastMessageFor(String destination) {
 List<String> all = 
 Maps.getOrDefault(messages(), destination, Collections.emptyList());
 return all.size() > 0 ? all.get(all.size() - 1) : "";
 }
 
 }
 
 // ...
 }
  38. class Inbox {
 
 // ...
 
 interface Input {


    Db snapshot();
 Observable<Event> source();
 }
 
 interface Output {
 Observable<Db> db();
 Observable<Event> events();
 }
 
 static Output db(Input input) {
 return new Output() { /* Data Flow + Redux */ };
 }
 
 // ...
 }
  39. private Observable<Db> redux(Db initState, Observable<Event> events) {
 return events
 .map(Inbox::toReducer)


    .scan(initState, (state, event) -> event.apply(state));
 }
 
 private static Transform<Db> toReducer(Event event) {
 switch (event.type()) {
 case "text-message:direct":
 return toDirectReducer(TextMessage.from(event));
 case "text-message:group":
 return toGroupReducer(TextMessage.from(event));
 default:
 return noop();
 }
 } interface Transform<T> extends Function<T, T> {}