Tuesday, December 2, 2014

Structuring Real-Life Applications

The Internet is full of smart peanut-size examples of how to solve X with "FRP" and Bacon.js. But how to organize a real-world size application? That's been asked once in a while and indeed I have an answer up in my sleeve. Don't take though that I'm saying this is the The Definitive Answer. I'm sure your own way is as good or better. Tell me about it!
I think there are some principles that you should apply to the design of any application though, like Single Reponsibility Principle and Separation of Concerns. Given that, your application should consist of components that are fairly independent of each others implementation details. I'd also like the components to communicate using some explicit signals instead of shared mutable state (nudge nudge Angular). For this purpose, I find the Bacon.js EventStreams andProperties quite handy.
So if a component needs to act when a triggering event occurs, why not give it an EventStream representing that event in its constructor. The EventStream is an abstraction for the event source, so the implementation of your component is does not depend on where the event originates from, meaning you can use a WebSocket message as well as a mouse click as the actual event source. Similarly, if you component needs to display or act on the state of something, why not give it a Property in its constructor.
When it comes to the outputs of a component, those can exposed as EventStreams and Properties in the component's public interface. In some cases it also makes sense to publish a Bus to allow plugging in event sources after component creation.
For example, a ShoppingCart model component might look like this.
function ShoppingCart(initialContents) {
  var addBus = new Bacon.Bus()
  var removeBus = new Bacon.Bus()
  var contentsProperty = Bacon.update(initialContents,
    addBus, function(contents, newItem) { return contents.concat(newItem) },
    removeBus, function(contents, removedItem) { return _.remove(contents, removedItem) }
  )
  return {
    addBus: addBus,
    removeBus: removeBus,
    contentsProperty: contentsProperty
  }    
}
Internally, the ShoppingCart contents are composed from an initial status and addBus and removeBus streams usingBacon.update.
The external interface of this component exposes the addBus and removeBus buses where you can plug external streams for adding and removing items. It also exposes the current contents of the cart as a Property.
Now you may define a view component that shows cart contents, using your favorite DOM manipulation technology, like virtual-dom:
function ShoppingCartView(contentsProperty) {
  function updateContentView(newContents) { /* omitted */ }
  contentsProperty.onValue(updateContentView)
}
And a component that can be used for adding stuff to your cart:
function NewItemView() {
  var $button, $nameField // JQuery objects
  var newItemProperty = Bacon.$.textFieldValue($nameField) // property containing the item being added
  var newItemClick = $button.asEventStream("click") // clicks on the "add to cart" button
  var newItemStream = newItemProperty.sampledBy(newItemClick)
  return {
    newItemStream: newItemStream
  }
}
And you can plug these guys together as follows.
var cart = ShoppingCart([])
var cartView = ShoppingCartView(cart.contentsProperty)
var newItemView = NewItemView()
cart.addBus.plug(newItemView.newItemStream)
So there you go!

8 comments:

  1. Could you please provide a full example if it possible.

    ReplyDelete
  2. By full you mean a fully working online store implemented in Bacon? Or just a runnable example? The latter you can easily hack together yourself and provide us a link. The former I have done too, but that's not open-source. On my Oredev slides, there's a small working shopping cart example: http://raimohanska.github.io/bacon-oredev/#/22 (click on Reveal, then Run)

    ReplyDelete
  3. Actually thought I'd set up a Bacon Store on the baconjs.github.io site, where you could buy .. er .. pictures of Kevin Bacon or something?

    ReplyDelete
  4. Yes, I mean a full application like a TodoMVC.

    ReplyDelete
    Replies
    1. Found this in github: https://github.com/hura/todomvc-react-baconjs


      Also, another reusable piece of code to give ideas how to do the integration of bacon and react here: https://github.com/jamesmacaulay/react-bacon



      Delete
  5. I would refactor all the buses away by turning all the inputs to be input arguments for the component. The proposed way reminds me too much of JavaBeans framework (data set with synchronous imperative functions, data modifications delivered with events).

    function ShoppingCart(initialContents, addItem, removeItem) {
    Bacon.update(initialContents,
    addItem, function(contents, newItem) { return contents.concat(newItem) },
    removeItem, function(contents, removedItem) { return _.remove(contents, removedItem) }
    )
    }

    var removeItemStream = ... // create a stream with event delegation
    var newItemView = NewItemView()
    var cart = ShoppingCart([], newItemView.newItemStream, removeItemStream)
    var cartView = ShoppingCartView(cart)

    ReplyDelete
    Replies
    1. That's a good point. It's probably a better idea not to expose Buses from the ShoppingCart component and to avoid them altogether as far as possible.

      Delete
  6. Oh and here's a post on Flux inspired data flow with React and Bacon.js. Includes link to TodoMVC implementation! http://blog.hertzen.com/post/102991359167/flux-inspired-reactive-data-flow-using-react-and

    ReplyDelete