alpheios-project / documentation

Alpheios Developer Documentation
0 stars 0 forks source link

Flux-like architecture in components #12

Open kirlat opened 5 years ago

kirlat commented 5 years ago

This is related to #10, but is slightly different in its focus. So I believe it's better to create a different issue for this so it won't get lost. Also, there are some "real life" implementation examples we can look at.

I was experimenting with different approaches and I think the one that works best is a Flux-like architecture: https://facebook.github.io/flux/docs/in-depth-overview.html. Flux was created for React and then selected by Vue.js for Vuex. I think it makes great sense to use for us too. It can work both with UI and data modules and without them, as I will show below.

Each data (or UI) module may expose the following to its clients (UI components, mostly):

VUEX STORE. Integrates into a common app's Vuex store as a module. This store is exposed to Vue UI components as this.$store. A. Reactive data These are properties of a Vuex store module. They should be used in Vue UI component templates directly. If data are not referred by Vue templates, it should be not here, but in the following section (non-reactive data that is). Example - a panel uses a visible prop of a panel UI module: https://github.com/alpheios-project/components/blob/comp-vue-refactoring/src/vue/components/panel.vue#L5

PUBLIC API. Integrates as module into Vue UI components with provide. Visible inside a Vue UI component as this.moduleName. Has to be claimed before use as inject: ['moduleName']). Example - Lookup claims API of ui, l10n, and settings modules: https://github.com/alpheios-project/components/blob/comp-vue-refactoring/src/vue/components/lookup.vue#L44 B. Non-reactive data These are properties that are not referred by Vue templates directly, There is no reason for them to be reactive. They can be used, however, by methods of Vue components. This props can also hold references to business data objects (but that should probably be avoided because it increases complexity). Example - a panel uses content options setting to check a panel position inside a Vue instance method: https://github.com/alpheios-project/components/blob/comp-vue-refactoring/src/vue/components/panel.vue#L384 C. Methods (Actions) These are functions that manipulate data and do other things (fulfill actions, i.e. opens a popup). Example - Lookup opens a popup: https://github.com/alpheios-project/components/blob/comp-vue-refactoring/src/vue/components/lookup.vue#L149

The Flux-like behavior forms a single-directional data flow: UI components reads reactive data -> UI component displays reactive data -> If components want to manipulate data, it calls public API methods (actions) -> Public API methods do some things they need to do and update reactive data -> UI components see the data change and update their presentation.

This creates a simple, predictable data flow that is the same for all components. It is easy to track and debug (all changes goes through a limited set of public API methods). Vue components will have no business logic inside, and this is what our goal is: to separate business logic from presentation and to keep business logic away from Vue components. Vue components are framework dependent and we want have them as lightweight as possible. This will allow to replace them with components based on other framework easily, if we have to.

It is also in line with what Vuex recommends for manipulating its data except we use public API methods instead of Vuex mutations and actions. Our methods can be considered as an "extended" version of Vuex mutations and actions.

The concept of Vuex store's public data and shard API creates a level of abstraction between UI components and implementation of business logic.

This architecture is flexible enough so that we can use it without modules yet expose exactly the same interface to the Vue UI components as if the modules were used. Here is an example of a UI controller exposing 'settings,app,ui, andlanguage` module-like groups to components: https://github.com/alpheios-project/components/blob/comp-vue-refactoring/src/lib/controllers/ui-controller.js#L332-L365.

Please let me know what do you think about it. Would such an architectural approach be good for us? Do you see any drawbacks with it? Do you have any comments or suggestions?

Thanks!

balmas commented 5 years ago

I agree that the Flux-style approach makes sense for our application.

I'm a little unclear those on this statement:

This architecture is flexible enough so that we can use it without modules yet expose exactly the same interface to the Vue UI components as if the modules were used. Here is an example of a UI controller exposing 'settings, app, ui, and language` module-like groups to components: https://github.com/alpheios-project/components/blob/comp-vue-refactoring/src/lib/controllers/ui-controller.js#L332-L365.

Does this mean that you are suggesting we drop the idea of using Vue modules to manage the different parts of the store?

kirlat commented 5 years ago

Does this mean that you are suggesting we drop the idea of using Vue modules to manage the different parts of the store?

No, I don't think we should stop using modules. But I think a "pseudo-module" approach can be a good addition to the modules concept. Here are pro and cons of both approaches: Modules Pros: Flexibility and configurability. We can choose what modules to use for each UI controller configuration. This will help to create different variants of controller for different tasks (if, let's say, inflection tables are not required, we do not load an inflections module). Also, module takes options as an argument. This allows whoever creates a UI controller (content script, for example) to configure modules in a specific way. As modules are separate entities, they can be tested independently. That's an advantage too. Cons: Little more complex as requires creation of a separate module class.

Pseudo-modules Pros: Simpler than creating a module. Cons: Whatever pros of modules approach we're missing 🙂

So I think generally we should use modules. I'm using "pseudo-modules" within a UI controller right now for things that are probably inseparable from a UI controller (such as state). In a way, UI controller is a module of itself as it needs to expose its functionality to UI components (a pseudo-module that is). Also, I'm using it as a temporary solution during refactoring for grouping functions similar by purpose that can then be separated into an independent module or for things that we cannot decide about right now (i.e. how to better present state of things like "word item").

If something is presenting a piece of independent functionality (as authentication, or inflection tables), it should make a module. If something is an inseparable part of a UI controller, it can be exposed as a pseudo-module (but we should keep that to the minimum).

Please let me know what do you think.

balmas commented 5 years ago

ok that makes sense, and I like the flexibility. I think I would like to move away from adhoc data structures towards a more standardized module system for data that is shared across components, so that we don't just end up with a repeat of what we have now, in a different format, but I like have the flexibility and agility to get things up and running quickly too.

irina060981 commented 5 years ago

I was using similiar structure before - controller + services (similiar to api modules) + vuex store (for controlling reativity for the whole vue-components stack) + vue-components And I liked it - because it decreases the whole amount of code and could give a useful way of abstarction.

In our case if it allow to avoid big data (pseudo-reactive) pops inside panel, popup and their children then it will make it valuable.

So I like the overall idea, I think it is a good approach!

Also I think that this code style needs additional efforts to make code more readable and understandable: 1) we need some name convensions for different parts of the application (for properties, actions inside modules and components) 2) we need proper error handling - as we decrease dencity of relations between parts of our code 3) we need both types of actions - create and clear data to track the whole data lifecircle to avoid inconsistent data, because we really don't have independent data - inflections, auth and other depends on each other 4) we need to use computed properties accurately with Vuex store values to use caching reactivity properties correctly 5) as we partly centralized storing data in Vuex and partly keep sending it through parent/child Vue reactivity - here should be some additional checks

Hope, I have understood everything correct and my though have sense here :)

kirlat commented 5 years ago

Thanks for you comments! Your thoughts raise some very important points and we need to think about how to approach it the best way (especially the error handling).

I hope a suggested approach can replace parent/child Vue data/event flows completely. This should make overall data streams simpler and more reliable.

irina060981 commented 5 years ago

I don't think, that it could be really removed parent/child reactivity completely, because we have single-instances objects (like popup/panel/grammar and so on) and multi-instance objects (like tooltips, infl-footnote, infl-attribute and so on). I don't think that cretion reactive properties inside Vuex for multi-instance components are a good solution, I believe vue-components reactive approach here is much better.

irina060981 commented 5 years ago

I was thinking about error handling for events - may be we should use some variant of a central event bus controller, that could register all events (pub/sub) and track all uncaught events, could have some events props checking? also it could have some debug mechanism to give an opportunity to debug events stack or may be to vizualize the event's stack in some journal-view?

Also we could create ErrorController, attach it to some other controllers and api's and collect errors there - it could give an opportunity to track errors, see their order, create error log and also it could have some actions to handle with errors (send a message, give template answers and so on)

What do you think?

kirlat commented 5 years ago

I don't think, that it could be really removed parent/child reactivity completely, because we have single-instances objects (like popup/panel/grammar and so on) and multi-instance objects (like tooltips, infl-footnote, infl-attribute and so on). I don't think that cretion reactive properties inside Vuex for multi-instance components are a good solution, I believe vue-components reactive approach here is much better.

You're right, for components like toolitps or footnotes "props down" approach makes a lot of sense. I've just omitted them because I did not consider it to be "serious" (i.e. business-logic related) data, but we definitely shall remember about such cases. I also think that it would make sense to have local reactive properties (i.e. "data") within some component, if those props are "private" for the component itself.

kirlat commented 5 years ago

I was thinking about error handling for events - may be we should use some variant of a central event bus controller, that could register all events (pub/sub) and track all uncaught events, could have some events props checking? also it could have some debug mechanism to give an opportunity to debug events stack or may be to vizualize the event's stack in some journal-view?

Also we could create ErrorController, attach it to some other controllers and api's and collect errors there - it could give an opportunity to track errors, see their order, create error log and also it could have some actions to handle with errors (send a message, give template answers and so on)

What do you think?

I think it's a great idea! A centralized journal that will record everything that happens in a system should work very well for error tracking. As we were discussing with @balmas, it would be beneficial to have several debugging flags each of those would turn debugging of some aspect of functionality on. For example, something like "Debug Auth On" would trigger recording of all authentication-related events. It would be advantageous if debugging can be turned on and off this way dynamically. Those events can also be shown within a "status" panel so that app users, who are experiencing issues, would be able to copy them to us. We could probably even add a "Send to Alpheios" button that will send all the recorded logs to us.

Maybe our logger class can evolve into something like this.

As for handling errors (i.e. the actual errors such as incorrect data, timeout, etc) we should probably try to stick with try/catch whenever possible. This is a standard error-handling mechanism in JS and we should follow its practices. But if we understand it is not enough, we could come up with our own solution.

What do you think?

irina060981 commented 5 years ago

I think the main problem with try/catch is in bubbling errors, it is not bubbling :) So when you catch an error you should do something here and return some result. And parent execution in stack could know and handle with that error, or couldn't even know about it.

Also different Promise issues (like used in client-adapters) could work without try/catch ability.

Finally we had a lot of separate error problems that won't lead to some overal error conclusion. And also they quietly dead finally if there is no console.error.

So I think try/catch, resolve/reject, onsuccess/onerror - they are different code styling and no more, but there is no overal error handling in javascript. Also I think we could collect user experience from errors using some simple API passing to some server and see user experience problems.

balmas commented 5 years ago

I like all of these suggestions.

Re the centralized event bus, @kirlat was the intent to use the UIEventController class for this?

Re errors, I think at a minimum, defining our best practices for error handling in difference scenarios (promises, etc.) would be a good step forward. That might also help us to see what a solution for managing them better overall might be for our application.

kirlat commented 5 years ago

Re the centralized event bus, @kirlat was the intent to use the UIEventController class for this?

As I understand the concept of the centralized event bus correctly, it's something that handles all events in an application. Then I think I would prefer a more specialized controller approach. I like the idea that we have several business objects (LexicalQuery, Authentication, etc.), each of those can publish events, and whoever is interested to those events subscribes to them. I think this approach allows to understand easier where the event was originated (in the event's publisher). It is also more modular, as we can add modules that publishes events easily. A centralized event bus (at least how I understand it) is something where all events are global and an event originator publishes an event to the centralized bus and then this bus redirects an event to its subscribers on behalf of event's originator. I think that's more complex and I would prefer a simpler solution if it satisfies our requirements 🙂. But sometimes a centralized event bus can has its advantages.

I was thinking we could use UIEventController as a specialized controller to handle UI events such as mouse/pointer movements, clicks/touches, etc.

irina060981 commented 5 years ago

I think that centralized event bus could be created in several ways.

I meant, that when a module publishes event and at the same time it sends a message to central EventBus Controller - "I published the event", when someone sibscribes to events - it at the same time sends a message to central EventBus Controller - "I subscribed to then event"

And EventBus doesn't controll the events workflow and doesn't have any specific bussiness logic. But it could track the events lifecircle, inform publisher that noone was subscribed to the published event and this way publisher could have a scenario when noone has got the event (or may not have such scenario).

This way EventBus has an event journal, could create event log or could give an event map.

kirlat commented 5 years ago

Implemented this way, it would make a lot of sense, I think

balmas commented 5 years ago

I agree.

balmas commented 4 years ago

this was implemented in 3.0