alpheios-project / documentation

Alpheios Developer Documentation
0 stars 0 forks source link

Modules of a UI controller #10

Closed kirlat closed 4 years ago

kirlat commented 5 years ago

I would like to present and offer for discussion (please consider this to be an unofficial RFC) a notion of modules in a UI controller. The concept of modules is described in the archictecture documentation. It also list reasons behind choice of certain design decisions of the module architecture. Please let me know if you have any questions or not agree with anything written there.

An example implementation of an L10n module is here. This part demonstrates methods that will be installed via Vue plugin to all UI components globally.

A UserAuth component has been updated to demonstrate the use of those global methods. It also has a dependency check implemented.

A UI controller registers the module, creates the store and mounts it to the panel component.

The major design goal was to have solution as simple as possible while retaining flexibility.

@balmas, @irina060981: Please let me know what do you think about this approach. Do you see any drawbacks or shortcomings in it? Do you see anything that can be improved? Thanks!

kirlat commented 5 years ago

Another important question to discuss is: would we prefer to have all available API of all modules (i.e. all methods to work with L10n, Inflections, etc.) to be installed as instance methods on every UI component, or would we like to enable it selectively (some UI components will have access to Inflections methods and some not).

With the former, development is arguably simpler: all available API methods combined (L10n methods + Inflection methods + etc.) are available to every UI component automatically. There is no need to analyze what needs to be activated in order to add, let's say, Inflections methods. If a component A uses method Z, we can copy and past this code to a component B and it will work the same.

However, it creates a greater pollution as we will have every API method installed on every UI component. But if we follow our convention on prefixing methods with the module name, I think it is manageable.

What is your opinion on this?

balmas commented 5 years ago

If I understand correctly, Data Modules in the architecture you propose are Vuex Modules. So their role is is essentially to encapsulate all state variables and mutations related to a specific business function and provide an API to the data controlled by that function. Is that correct?

I think this makes sense, but I also think it can be very easy to get lazy and start putting business logic into those state modules rather than making sure it is in the underlying business library. We will have to be careful about this.

balmas commented 5 years ago

It's less clear to me what the UI Modules are and how they different from what we have currently. I think that these are NOT related to Vuex explicitly and are essentially just a way of organizing all code related to the major composite ui components into one piece so that they can be more easily composed and that they are decoupled from the ui controller. Is that correct?

balmas commented 5 years ago

Another important question to discuss is: would we prefer to have all available API of all modules (i.e. all methods to work with L10n, Inflections, etc.) to be installed as instance methods on every UI component, or would we like to enable it selectively (some UI components will have access to Inflections methods and some not).

With the former, development is arguably simpler: all available API methods combined (L10n methods + Inflection methods + etc.) are available to every UI component automatically. There is no need to analyze what needs to be activated in order to add, let's say, Inflections methods. If a component A uses method Z, we can copy and past this code to a component B and it will work the same.

However, it creates a greater pollution as we will have every API method installed on every UI component. But if we follow our convention on prefixing methods with the module name, I think it is manageable.

What is your opinion on this?

Are there any performance or memory implications to the decision? I.e. does it consume significantly more memory to have all modules injected into the global space?

kirlat commented 5 years ago

If I understand correctly, Data Modules in the architecture you propose are Vuex Modules. So their role is is essentially to encapsulate all state variables and mutations related to a specific business function and provide an API to the data controlled by that function. Is that correct?

This is correct. Those are Vuex modules encapsulated into a class. This class will then be instantiated by a UI controller during module registration and it's store property will be mounted into a global Vuex store of a UI controller within a controller's init() method. After that, all methods of a module will become available to all other modules of the app, because all of them use the same store. Data modules provide a bridge between "regular" JS functions of libraries and the Vuex world.

I think this makes sense, but I also think it can be very easy to get lazy and start putting business logic into those state modules rather than making sure it is in the underlying business library. We will have to be careful about this.

I agree this can be an issue. Not sure what we can do to protect ourselves against it. We might mull it over. Ideally, if library methods and data would map just into Vuex store props, getters, and actions there will be no place for business logic inside a components, but I'm not sure how feasible is this and what effort it will require.

It's less clear to me what the UI Modules are and how they different from what we have currently. I think that these are NOT related to Vuex explicitly and are essentially just a way of organizing all code related to the major composite ui components into one piece so that they can be more easily composed and that they are decoupled from the ui controller. Is that correct?

Yes, that's correct. It's just a way to define a major UI component so that it can be loaded and unloaded easily. It is also a way to remove direct calls and direct data access of UI controller methods and props from UI components.

Current implementation of webextension has two components: a panel and a popup. They are independent in a way that they can be, in theory, used separately from each other. We could use a panel without a popup, or we could use a popup without a panel. Even though a popup alone will not display all the information we have on a word, it still can be (in theory) sufficient for some applications. Of course, we could also use both panel and popup as well.

If we would create a mobile version of a popup later, we could load a mobile popup with a regular panel on devices with sparse screen estate and load a regular popup with a panel on desktops. We could mix and match them in any way possible. And creating such configurations would require just couple lines of code.

Modules are, in general, just logical groups of some related pieces of functionality. Whatever is grouped into modules can be mounted or unmounted easily. That's the major raison d'etre for modules. But they have other advantages too.

Without modules, we could do all this in a more straightforward (but not sure a simpler) way: UI controller could mount all data props of libraries (L10n, Inflection, etc.) into the store so that it will be accessible by UI components. UI controller could also provide methods to retrieve data in a way similar to the one used now, but make those methods available via a Vuex store. Panel and popup could be not put into modules too: we could keep them as props of a UI controller as we do now. If we need a different configuration of UI elements, we can do that by providing a different UI controller class that will configure it differently. So instead of having one unified UI controller that loads whatever modules are required, we will end up with different versions of a UI controller class, each one for its own specific purpose: one UI controller for mobile devices, one for desktop. another one for a PWA. But I think it will be a worse approach, less maintainable. In fact, we're using it for PWA now: PWA has its own version of a UI controller. It was once very close to the one of webextension, but then changes between them grew. Now they are very different and it will take a significant effort to port latest updates from webextension's UI to the PWA. I'm not this (un-modular) approach is a better one.

Modular architecture, even though it requires little more code, has several advantages:

  1. It groups similar functionality together. With modules, we know that all L10n methods are in the L10nModule class and nowhere else. It makes it easier to understand what methods are available by inspecting the module code. It also makes it easier to localize changes in an underlying library: if L10n lib has to change, we will know that only methods of L10n module would need to be updated to accommodate this change. No need to update anything else.
  2. Modules isolate underlying libraries. If we decide to replace our current library with something else (let's say IntlMessageFormat we're using now gets abandoned and we decided to switch to some other library), we could still do that keeping module API the same. That will allow to require no change from the components that uses this API.
  3. Modules allow to load chunks of functionality on demand, even dynamically, creating a set of functionality that we need from a UI controller.
  4. Modules allow easy versioning. If for whatever reason we decide to do a radical change in an underlying library architecture, we could create two versions of it's modules: V1 that is backward compatible with its existing consumers and V2 that will have all the latest and greatest features. Then we can slowly start updating consumers to use the V2, and, when every consumer is converted, sunset the V1.
  5. Modules are much easier to test.

The choice of UI architecture is a very important decision that can have a long lasting effect That's why I think we should discuss it thoroughly, analyzing every weakness and every possibility to improve it. We probably better spend some time on it now than bump into a wall later. We need to be sure there is nothing we're overlooking.

kirlat commented 5 years ago

Are there any performance or memory implications to the decision? I.e. does it consume significantly more memory to have all modules injected into the global space?

I'm not sure how it works under the hood, but I think it just creates methods on components that translate calls into the original ones. Should not require much memory for that, and performance implications should be negligible, I think. My biggest concern would be that it "pollutes" a component's namespace with many extra methods leading to the outside, but once they're prefixed, it's probably not that bad. window has it much worse, having many methods mounted, but still creates no problems in use.

irina060981 commented 5 years ago

I think it is a big change to the code, to the code styling. Such architecture becomes centralized and will have one main overal dependency - uiController.store. Am I right that it will lead to use UIController in all data and ui modules as a dependency?

And how would it be able to attach components from a separate libraries?

If to be honest, I think it is a great challenge to implement such an arcitecture and be sure to envisage all posible effects. Because it would be created around a third-party tool (Vuex) and we don't know what it has under the hood now and will have in future. So I think it is an interesting and lovely and risky solution.

How it will work with other features - cmd tools for lexical testing, embed-lib, games and others, and how it uses memory we will see only in practice.

I was working with Vuex, I liked this solution. But it seems to me that all mutations are saved in RAM and are controlled there, so I used it in not great solutions (I had always a task to decrease memory usage) and had no problems. How it will work in different environments - with more or less RAM - we will see :)

kirlat commented 5 years ago

Am I right that it will lead to use UIController in all data and ui modules as a dependency?

Data modules would have no dependency on a UI controller at all (at least that's the plan). The purpose of data modules is to supply data. Each data module declare its own store piece, and then a UI controller, by registering a module, inserts this store piece into a common Vuex store (maintained by a UI controller). All data module needs to know is to update its own store piece when it receives a request to do so (by some of the data consumers calling one of its actions). So data module has no dependency on neither the UI controller, nor on the common Vuex store.

UI modules need to access data from the Vuex store. So they have a dependency on not the UI controller itself, but on a Vuex store (which is part of a UI controller, but UI modules do not care about that).

It all goes as follows: UI modules uses data module's methods to ask the module to pull some data. Data modules retrieve requested data and puts it to the module's segment of a common Vuex store. As all those segments are inserted into a global Vuex store, the data retrieved becomes available to UI modules. And UI modules uses this data to update their views.

And how would it be able to attach components from a separate libraries?

Each separate library would need to create a "module class": a wrapper around library methods that will retrieve data by request and store it to the Vuex store. When a UI controller registers the module, it integrates library module's store into a common one and exposes methods of a library's module (i.e. the wrapper) to the UI components.

I was working with Vuex, I liked this solution. But it seems to me that all mutations are saved in RAM and are controlled there, so I used it in not great solutions (I had always a task to decrease memory usage) and had no problems.

That's a very valid concern. From what I know, Vue reactivity is fast and efficient, but can require a significant amount of memory. That's because its uses a virtual DOM. So I think we should be very careful to keep an amount of data exposed to UI modules (i.e. store data that is reactive) to be as small as possible. All other data necessary for data module's functionality should be kept inside a data module object itself and not be mounted into the store. It will not be a subject of reactivity then and this will allow us to avoid reactivity RAM overhead.

balmas commented 5 years ago

summarizing a couple of additional decisions from today's meeting:

  1. we will use a shared store and decouple the idea of how and where the data is to be displayed from what the data is. I.e. there will not be separate buckets for data to be made available to the popup vs panel. The exception to this might be if there is UI-component-specific state data that needs to be reactive and available in the store, such as the current tab on a panel. For this data we could have a ui-component specific store module, if necessary. (we didn't explore this last point too thoroughly in the call, so @kirlat and @irina060981 please feel free to comment on this)

  2. we will proceed with this design incrementally, keeping a close watch on the RAM overhead, and reassess as we go. Whether we use Vuex or not, a central state management solution is necessary as the application grows so starting with this design makes sense.

balmas commented 4 years ago

vuex has been implemented. UI Controller needs to be further refactored now but that should be discussed in a separate issue.