EventBus/PubSub vs (reactive extensions) RX with respect to code clarity in a single threaded application

前端 未结 4 1503
有刺的猬
有刺的猬 2020-12-22 22:42

Currently, I am using an EventBus/PubSub architecture/pattern with Scala (and JavaFX) to implement a simple note organizing app (sort of like an Evernote client with some ad

相关标签:
4条回答
  • 2020-12-22 22:56

    I think you have to use the rxjava because it provides much more flexibility. If you need a bus you can use an enum like this:

    public enum Events {
    
      public static PublishSubject <Object> myEvent = PublishSubject.create ();
    }
    
    //where you want to publish something
    Events.myEvent.onNext(myObject);
    
    //where you want to receive an event
    Events.myEvent.subscribe (...);
    

    .

    0 讨论(0)
  • 2020-12-22 23:04

    As per my comment above, JavaFx has a class ObservableValue which sort of corresponds to RX Observable (probably ConnectableObservable to be more precise, as it allows more than one subscription). I use the following implicit class to convert from RX to JFX, like this:

    import scala.collection.mutable.Map
    import javafx.beans.InvalidationListener
    import javafx.beans.value.ChangeListener
    import javafx.beans.value.ObservableValue
    import rx.lang.scala.Observable
    import rx.lang.scala.Subscription
    
    /**
     * Wrapper to allow interoperability bewteen RX observables and JavaFX
     * observables. 
     */
    object JfxRxImplicitConversion {
      implicit class JfxRxObservable[T](theObs : Observable[T]) extends ObservableValue[T] { jfxRxObs =>
        val invalListeners : Map[InvalidationListener,Subscription] = Map.empty
        val changeListeners : Map[ChangeListener[_ >: T],Subscription] = Map.empty
        var last : T = _
        theObs.subscribe{last = _}
    
        override def getValue() : T = last 
    
        override def addListener(arg0 : InvalidationListener) : Unit = {
          invalListeners += arg0 -> theObs.subscribe { next : T => arg0.invalidated(jfxRxObs) }
        }
    
        override def removeListener(arg0 : InvalidationListener) : Unit = {
          invalListeners(arg0).unsubscribe
          invalListeners - arg0
        }
    
        override def addListener(arg0 : ChangeListener[_ >: T]) : Unit = {
          changeListeners += arg0 -> theObs.subscribe { next : T => arg0.changed(jfxRxObs,last,next) }
        }
    
        override def removeListener(arg0 : ChangeListener[_ >: T]) : Unit = {
          changeListeners(arg0).unsubscribe
          changeListeners - arg0
        }
      }
    }
    

    Then allows you to use property bindings like so (this is ScalaFX, but corresponds to Property.bind in JavaFX):

    new Label {
        text <== rxObs
    }
    

    Where rxObs could be for example:

    val rxObs : rx.Observable[String] = Observable.
      interval(1 second).
      map{_.toString}.
      observeOn{rx.lang.scala.schedulers.ExecutorScheduler(JavaFXExecutorService)} 
    

    which is simply a counter that increments every second. Just remember to import the implicit class. I can't imagine it getting any cleaner than that!

    The above is a bit convoluted, due to the need to use a scheduler that plays nicely with JavaFx. See this question for a link to a Gist of how JavaFXExecutorService is implemented. There is an enhancement request for scala RX to make this into an implicit argument, so in future you may not need the .observeOn call.

    0 讨论(0)
  • 2020-12-22 23:12

    I learned one or two things since I asked this question 2 years ago, here is my current understanding (as explained in Stephen's FRP book):

    The both try to help to describe a state machine, i.e. describe how the state of the program is changing in response to events.

    The key difference between EventBus and FRP is compositionality:

    • What is compositional ?

      • Functional programming is compositional. We can all agree on that. We can take any pure function, combine it with other pure functions and what we get is a more complicated pure function.
      • Compositionality means that when you declare something then at the site of the declaration all behaviour of the declared entity is defined.
    • FRP is a compositional way of describing a state machine, and event-bus is not. Why ?

      • It describes a state machine.
      • It is compositional because the description is done by using pure functions and immutable values.
    • EventBus is not a compositional way of describing a state machine. Why not ?

      • You cannot take any two event busses and compose them in a way that gives a new, composed event bus describing the composed state machine. Why not ?
        • Event busses are not first class citizens (as opposed to FRP's Event/Stream) .
          • What happens if you try to make event busses first class citizens ?
            • Then you get something resembling FRP/RX.
        • State influenced by the event busses are not
          • first class citizens (i.e. referentially transparent, pure values in contrast to FRP's Behaviour/Cell)
          • tied to the event busses in a declarative/functional way, instead state is modified imperatively triggered by event handling

    In summary, EventBus is not compositional, because the meaning and behaviour of a composed EventBus (i.e. the time evolution of the state that is influenced by said composed EventBus) depends on time (i.e the state of those parts of the software which are not included explicitly in the declaration of the composed EventBus). In other words, if I would try to declare a composed EventBus then it would not be possible to determine (just by looking at the declaration of the composed EventBus) what rules govern the state evolution of those states that are influenced by the composed EventBus, this is in contrast to FRP, where this can be done.)

    0 讨论(0)
  • 2020-12-22 23:18

    The following is what I see as benefits of using reactive event streams in a single-threaded synchronous application.

    1. More declarative, less side-effects and less mutable state.

    Event streams are capable of encapsulating logic and state, potentially leaving your code without side-effects and mutable variables.

    Consider an application that counts button clicks and displays the number of clicks as a label.

    Plain Java solution:

    private int counter = 0; // mutable field!!!
    
    Button incBtn = new Button("Increment");
    Label label = new Label("0");
    
    incBtn.addEventHandler(ACTION, a -> {
        label.setText(Integer.toString(++counter)); // side-effect!!!
    });
    

    ReactFX solution:

    Button incBtn = new Button("Increment");
    Label label = new Label("0");
    
    EventStreams.eventsOf(incBtn, ACTION)
            .accumulate(0, (n, a) -> n + 1)
            .map(Object::toString)
            .feedTo(label.textProperty());
    

    No mutable variable is used and the side-effectful assignment to label.textProperty() is hidden behind an abstraction.

    In his master thesis, Eugen Kiss has proposed integration of ReactFX with Scala. Using his integration, the solution could look like this:

    val incBtn = new Button("Increment")
    val label = new Label("0")
    
    label.text |= EventStreams.eventsOf(incBtn, ACTION)
        .accumulate(0, (n, a) => n + 1)
        .map(n => n.toString)
    

    It is equivalent to the previous, with the additional benefit of eliminating inversion of control.

    2. Means to eliminate glitches and redundant computations. (ReactFX only)

    Glitches are temporary inconsistencies in observable state. ReactFX has means to suspend event propagation until all updates to an object have been processed, avoiding both glitches and redundant updates. In particular, have a look at suspendable event streams, Indicator, InhiBeans and my blog post about InhiBeans. These techniques rely on the fact that event propagation is synchronous, therefore do not translate to rxJava.

    3. Clear connection between event producer and event consumer.

    Event bus is a global object that anyone can publish to and subscribe to. The coupling between event producer and event consumer is indirect and therefore less clear. With reactive event streams, the coupling between producer and consumer is much more explicit. Compare:

    Event bus:

    class A {
        public void f() {
            eventBus.post(evt);
        }
    }
    
    // during initialization
    eventBus.register(consumer);
    A a = new A();
    

    The relationship between a and consumer is not clear from looking at just the initialization code.

    Event streams:

    class A {
        public EventStream<MyEvent> events() { /* ... */ }
    }
    
    // during initialization
    A a = new A();
    a.events().subscribe(consumer);
    

    The relationship between a and consumer is very explicit.

    4. Events published by an object are manifested in its API.

    Using the example from the previous section, in the event bus sample, A's API does not tell you what events are published by instances of A. On the other hand, in the event streams sample, A's API states that instances of A publish events of type MyEvent.

    0 讨论(0)
提交回复
热议问题