Closed kristianmandrup closed 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! 💃 🥇
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.
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.
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?
Yes, I called it stateobserver
:)
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
?)
An event is passed to a global event bus, essentially a global stream of events
Then there are other streams subscribing to this event bus and filtering:
storeEventStream
(event bus)
remoteEventStream
(event bus)
...
the remoteEventStream
may pick up an event with the signature remote
and use it to make a call to a remote system or perhaps interact with some other remote system transportation protocol (websocket etc.)
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.
the store event stream may filters global event stream for store
events. It can then use events to update its internal state. After updating the state, it can signal the state change to other system parts via yet another event (fx. via the storeObserver
or storePathObserver
)
each component is ideally "stupid" and only knows how to update its display (like tables/rows in a DB). All business logic off loaded to services. A component and service can be hooked up. When a service receives an event of interest, it calls the component subscribing to it with some data that the component uses to update its local state. This way components are completely decoupled from any system knowledge and easy to test. All "business logic" ideally entirely encapsulated in services that can be interchanged at will. Services only subscribe to generic even signatures, also oblivious to full system state. Full decoupling.
This would be the ideal architecture for a complex system where many devs can operate independently without access to full system.
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) (viaactionObserver
?)
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 onremoteEventStream
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 ?
This architecture goes way beyond MobX and doesn't aim to use state observers in any form.
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:
Currently the component is coupled directly to a specific store path. If we change the store path structure, tough luck!
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?)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. Thecallback
could be used in thethen
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
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!
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!