ivanrod / polobx

A state manager for Polymer based in MobX
MIT License
23 stars 3 forks source link

Suggestion: Learning from Micro Services - subscribe to message patterns instead #6

Closed kristianmandrup closed 7 years ago

kristianmandrup commented 7 years ago

Lately been looking into MicroServices best practices, such as Seneca framework and been reading the Tao of Micro Services. We are basically trying to achieve the same thing on both ends but we haven't quite got it right on the frontend yet. Mobx is a huge step in the right direction! Thanks for bringing it to Polymer 2 :)

However:

  signal: (msg, data) => {
    this.bus.push({msg, data, store: this})
  }
  actions: {
    addOne: function(payload) {
      let counter = this.store.counter++
      let todo =this.store.todos[counter]
      this.signal({msg: {model: 'todo', event: 'wasAdded', level: payload.level}, data: todo)
    }
  }

Currently the component is coupled directly to a specific store path. If we change the store path structure, tough luck!

      properties: {

        myVar: {
          type: String,
          statePath: {
            store: 'myStore',
            path: 'foo'
          }
        },

The statePath.path introduces a very tight coupling between the data store and the component. Why not subscribe to an event msg pattern, then from the incoming event update component local state. The above could/should still be valid, just add this alternative (as an extra behaviour?)

stateObservers: {
  todoAdded: observe({
    store: 'myStore',
    // pattern can contain wildcards regular expr, or even be a function itself
    select: {
      model: 'todo', 
      event: 'wasAdded'
    }}, ({todo, app}) => {
     // some guard clause relevant to this component/event
     if (app.user.loggedIn) {
       // here we reference the message payload
       // we could also reference an entry in store (more direct coupling)
       this.todos.push(todo); 
     }
  }),
  ...

This introduces much more control and is way more powerful IMO. Also reduces need for computed properties, essentially like an even stronger combination in one.

The observe is a factory method that generates a function. The callback could be used in the then of a promise or whatever we like (abstracted) and gives us full control of if/how we update the local state on receiving such a message.

We would then need some sort of MessageBus (ie message queue) just like on the backend, where observers (services) all take a look at each message to see if it is of interest to them to react on.

bacon.js bus

Bus is an EventStream that allows you to push values into the stream. It also allows plugging other streams into the Bus. The Bus practically merges all plugged-in streams and the values pushed using the push method.

The MessageBus could therefor just be another EventStream... Querying Observable Sequences

Essentially each component could set up Stream subscribers to the global application state/change stream (aka Bus) and use filters to sort out what is of interest... :)

What do you think? I'd like to help in this effort. Finally we can get to a point where we have similar architectures on front- and backend :)

This only shows it at the store to component level, but the same principle could be use for dataflow between components for the entire system, exactly like the Micro Services model :) No more direct getter/setter bindings and tight coupling required.

In fact, why not go "all in" and make the store a service as well, following the Micro Services model. Then you would have essentially an internal client API gateway for components treated as Micro Services :) Wow!

  actions: {
    addOne: function(payload) {
      let msg = {msg: {model: 'todo', level: payload.level}
      this.storeSignal('increaseCounter', msg)

      // the following should be moved to the store service!
      let todo =this.store.todos[counter]
      this.signal('wasAdded', msg, data: todo)
    }
  }

Now the store could react to the signal in the same fashion and update the graph, perhaps save the action in localstore via another message/service etc. Full composition and flexibility. No hardcoded architecture.

Even better idea. Have one event bus per domain model and one for the store, then have components subscribe to one or more event buses to be more targeted and keep down the noise :) Simply merge streams of each subscribed bus into one, then listen. Wow!

kristianmandrup commented 7 years ago

In fact, thinking more about this... even better to abstract the component message observers into independent (reusable) services. Then we might almost end up with similar/same services and messages on both front and back end... and components are like a micro service data store to a backend service! The internal component state based DOM updates (via local databinding etc). are the side effect, like the updated to the DB. Crazy beautiful! 💃 🥇

kristianmandrup commented 7 years ago

For easy testing, the component should just work with regular methods we can call directly from the outside to make it "tick". When we then add the magic behaviour, the methods are linked to the services, ie. a service matches a message, and reacts by callings its subscriber components. Awesome! Plug and play!

The components should be completely stupid, only doing data binding, listening to and dispatching events. All business logic in services, just like on backend. Makes perfect sense. Essentially you have the same architecture as on backend but kind in reverse, flowing from data model into each leaf component. On backend, feeding into leaf components and then all the way down to an aggregation store (similar to front-end unified datastore) at the bottom.

ivanrod commented 7 years ago

I agree that we could add more flexibility on that point. I was thinking into exposing MobX autorun in the behavior but maybe this kind of solution could be a better approach.

Essentially each component could set up Stream subscribers to the global application state/change stream (aka Bus) and use filters to sort out what is of interest... :)

IMHO we could achieve this simply using autorun to subscribe to any state changes and maybe to actions.

Thus, we could have 3 types of state observers:

stateObservers: [
  // Action observer
  {
    store: 'myStore',
    action: 'myStoreAction',
    cb: actionResult => {
      this.activeSpinner = true;

      actionResult.then(() => {
        this.activeSpinner = false;
      });
    }
  },

  // Store path observer
  {
    store: 'myStore',
    path: 'authData',
    cb: (authData, state) => {

      if (authData.loggedIn) {
        this.myVar = authData.foo;
        this.myOtherVar = state.otherStateVar;
      }
    }
  },

  // Store observer
  {
    store: 'myStore',
    cb: state => {

      if (state.authData.loggedIn) {
        this.myGreatVar = state.otherStateVar;
      }
    }
  },

]

stateObservers could act like polymer observers but 'listening' to global state changes.

What do you think? Is it similar to your approach? I'll create a gitter channel, so we can chat about this :) .

Also, I’m creating a branch to work on this feature.

kristianmandrup commented 7 years ago

Great! Looks like some good ideas. Nothing set in stone! Let's experiment and find a good flexible architecture that is composable and simple yet avoids tight coupling. I will read up on MobX... Did you create the branch already?

ivanrod commented 7 years ago

Yes, I called it stateobserver :)

kristianmandrup commented 7 years ago

I propose the following:

Each component can dispatch actions that are picked up by the global actionHandler (aka reducer)

The actionHandler calls a reducer for each action which reduces the action to one or more events (or ignores, logs it whatever) (via actionObserver ?)

A remote system service, subscribing to particular events on remoteEventStream or better using a sub-stream that filters on remoteEventStream to generate a stream of events for that service to use.

When remote service gets data from remote system, it again passes data onto event stream which is picked up by stores or other services.

This would be the ideal architecture for a complex system where many devs can operate independently without access to full system.

ivanrod commented 7 years ago

I propose the following:

  • Take out the store from the MobX action reducer

Each component can dispatch actions that are picked up by the global actionHandler (aka reducer)

The actionHandler calls a reducer for each action which reduces the action to one or more events (or ignores, logs it whatever) (via actionObserver ?)

It sounds cool to me, similar to Redux approach.

A remote system service, subscribing to particular events on remoteEventStream or better using a sub-stream that filters on remoteEventStream to generate a stream of events for that service to use.

The event system you are proposing seems different from MobX approach, listening to events streams instead of state changes. Are you thinking of a message system similar to Backbone.Radio ?

kristianmandrup commented 7 years ago

This architecture goes way beyond MobX and doesn't aim to use state observers in any form.