-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Component (Object) Architecture #3
Comments
I think this is a great start at defining architectural best practices for us to follow, and providing a template for business components. I also really like the idea of isolating business logic from the display. I am becoming a little uneasy with the depth of our dependency on the Vue framework, and decoupling the business logic from the display logic will also help us to not become too dependent upon any one framework. |
I agree with Bridget - it is a great start for creating a higher-level application :) I have some thoughts about this approach:
I think that if we want to become less Vue-dependable and be ready to be used with different frameworks - we should reduce the data in Vue components. I think that we should use Vue components as a view templates that is only rendering input data, sending events and re-render on changing input data.
|
Great ideas, Irina, thanks for sharing! Agree with every word you said. Creating a Vue infrastructure we should also keep in mind its memory consumption. This probably is a topic for another discussion so I wont' go deep into that. A simple example could be an inflection table. Vue rendering is lazy, and Vue uses node patching. This means that if we have a rendered inflection table, it will stay in memory even if we switch to the other tab or work in a popup. It has many cells. If each cells has references to some other objects, those objects would stay in memory too. And those objects could refer to some other instances too. It could be even strong circular references. That all could eat up lots of memory! Should take those aspects into consideration when deciding on our architecture too. For localization we have L10n, so we could probably add more functionality to it if needed and move it to become a separate library. It's also so great that you mentioned styles! I think this is a very important part of an architecture (even though it might be orthogonal to the JS objects it does not matter). We probably need to have a different doc on styles too. I agree that it would probably make sense to separate styles from components (even though components, especially reusable ones, should probably have its own minimal styling to be of any value) but have no idea so far how it's best to be done. it's a complex topic as styles goes through a UI of several apps. The big issue in styles is naming. We're using BEM approach, but that requires to identify and separate visual components form one another. Should pay attention to that. We probably need to have at least two layers of styles: default ones and the ones that represent skins. We should also try to minimize rendered CSS if possible. Maybe we can peek at what large CSS frameworks (Bootstrap, UIkit, etc.) do here to borrow their expertise? Or maybe there is something even better than that? Another interesting technology that might benefit our visual components are CSS modules: https://css-tricks.com/css-modules-part-1-need/. Maybe we should research this topic too when we can. |
From my experience our approach to styling is close to used in Bootstrap:
In our case It seems to me that we couldn't really optimize styles amount - as they should be compiled before and append through the only css file to head. And BEM approach makes additional amount of memory that needs to save our styles (because of long classes name). |
Moved styles-related discussion to #4 to keep this issue focused on component (object) architecture. |
@balmas, @irina060981! Please review a proposal to update a business components architecture: https://github.com/alpheios-project/documentation/blob/master/development/app-architecture.md (described in Business components architecture section). Thanks! |
@kirlat , I have read your deatiled description only now (I am sorry, it seems I have missed an email about this update). I think that you are doing a great job describing all here with detailed schemas!
|
@irina060981! Thank you so much for your detailed answers. They're great pieces of information and super helpful. I would like to mull over it a little. I will try to answer more simple questions here 😄. Will start from the second one about a UI controller. I should probably travel back in time to explain it. When this project started, we were not using Vue.js yet. All visual components (and inflection tables as the most complex of them) where generating their HTML code manually. So we had components that rendered HTML of inflection tables, panel, popup. Those, in terms of Vue and similar frameworks where presentation components: the ones whose main purpose was to display something. However, we needed some controller to tie them up together. That's why UI controller was created. So if someone selected a different inflection table from a drop down, UI controller would intercept that and send a command to inflection tables library to generate and display a different table. Thus, the UI controller is something what Vuex is to Vue presentational components. Now we migrated all our presentational components into Vue SFC format, but UI controller still remained what it was. But I think once we start using Vuex, most of it's functionality, if not all, would go to Vuex. So the UI controller would not exist as a "supercontroller" what it currently is. Also, I want to note that the purpose of a UI controller was user input coordination only, If one wants to get lexical data via a lexical query, he/she should not use UI controller at all. That's what queries are for: to get information from one or several sources. So if one wants some lexical data, there is no need for a use UI controller at all. One should start a new LexicalQuery, and it should return all lexical data obtained. If somebody wants something like annotations, he/she should use AnnotationQuery and so on. One specialized query per type of data. That was the idea. Now, unfortunately, it's not so pure: LexicalQuery is coupled heavily with a UI controller as it calls multiple methods of it in the process. But I think we could (and should) fix that. If for your tasks you don't need any visual data display, then you don't need an instance of a UI Controller at all. You need to use Queries to get the data you need and that's it. If you want to display some different UI components in some different way, you probably need a different UI controller for that. It can inherit from a base UI controller component, or we could use composition where a UI controller would be composed of different modules instead. But maybe we would Vuex to replace a UI controller functionality. That's the idea. Implementation may differ from what's described, but it doesn't mean we couldn't fix it. The answer to your first question about singletons lies in area of ownership and memory management. When something that exists in memory for a long time (like a UI controller) owns some other object that occupy a sizeable chunk of memory, that other object cannot be garbage collected. My idea was that instead of holding some adapter for a long time, it would make sense to get a link to it only when we need it (inside of a lexical query, for example) and then release it (when lexical query is done) so it can be garbage collected. And singleton pattern, as I thought, would simplify reference management. If we have a reference to a morphAdapater, for example, and we store a reference to it in a UI controller and then we need to pass it to a LexicalQuery, we have to pass it as an argument. That could become cumbersome when we need to pass several references this way. It might be beneficial just to call What do you think? Does it make sense? |
@kirlat, thank you for the history explanation - it is really interesting for me as for a newby :) I understand (thank you for the story) that it is only a step on a upgrading/developing way :) About instances - it is not really clear for me - because - you are to pass the whole class instead of passing the object, and it will store almost the same structure for the class in the memory. May be there will be some benefit from not storing current values. I think that we could reach some benefits if use adapters the same way as axios Then all instance with the data of the whole class will be used inside closure and will be cleared after LexicalQuery object destroyed. |
I think we should eliminate UI Controller dependencies within LexicalQuery, if possible. Maybe we should even split lexical query functionality into several more specialized subqueries. I think it's dependence on UIController is bad. Regarding axios, are you suggest to use something like
where a static method creates an instance with specified parameters? It's something close to a builder pattern that is so popular in Java. I think this can be a good approach. |
Happy to see this discussion evolving! I think ultimately we want the UIController (or whatever it ends up being called) to be where Alpheios-specific business logic that crosses components and state lives. This is business logic that we want to be accessible to and consistent across all forms of Alpheios applications. I'm not sure what portion of it should move to Vuex and what should stay in the UIController but my instinct is that Vuex is responsible for managing and sharing state across the Vue components and the UIController is responsible for describing how and when that state needs to be shared. I think we might begin to really understand how this is needed once we add user data into the mix. But for example, the interaction between the lexical query results, the grammar, the inflection tables, annotation resources, etc. should be consistent across all applications that use Alpheios. Not all applications should be required to have all of these resources, but for whichever of them they do have, the interaction should be consistent. |
So the individual applications (Embedded library, PWA, Webextension, etc) need to be able to specify which components they want to include, which services those components should use and how they are configured, but should not have to wire them up together or have to know how they interact. |
And once we have the ability to store user data, we will want to be able to keep track of user interactions across components. For example, maybe certain sequences of steps (look up a word, access inflection tables, access grammar) would be combined to form a template for a future pedagogical exercise. Or to be recorded as one complete user experience. The details of this experience may differ depending upon which application they are using, but we will want to be able to have consistency in the way it is managed. |
One of the problems we have with a UIController right now is tight coupling between its modules. For example, LexicalQuery calls multiple methods of a UIController. Because of this, LexicalQuery cannot be used independently. To make LexicalQuery independent, we could make it return all data at the end as a return value. But this will be far from optimal. For example, this will prevent us from displaying pieces of query results as they arrive. To solve this, we can switch to an event-based communication between components. So if something, like a UIController, wants to receive pieces of data from LexicalQuery as they arrive, it subscribes to LexicalQuery events. If LexicalQuery fetches lemma translations, for example, it will fire a As a result, LexicalQuery is fully decoupled from a UIController. If nothing subscribes to a LexicalQuery events, it will still go on its own (although that'd be a wast of time, of course, but still). On the opposite end, it can have multiple components listening to events of a LexicalQuery, but this still would make no difference to LexicalQuery. It will notify all its subscribers. Also, adding or removing listening components would require no change in LexicalQuery's code. This all can go a long way between many existing components. @balmas, @irina060981: would that be a worthy approach? Do you see any drawbacks in it? |
I think there could be 2 approaches (according to the discussion):
2)UIController and LexicalQuery are independent - they could be used one without another or could be used together. And there are some very generic methods to communicate - for example using events. The main problem with generic events is that events handling is not a standard mechanism for all used platforms - chrome and safari has some difference between using events and node version should have node-way events. It seems to me that creation of such overall bus events could be not an easy solution. Also let's imagine in what cases we should use LexicalQuery separated from UIController - for LexicalQuery we need different options (language, vocabular preferences for definitions), need some localization for passing messages and we need to have define the output rules for different parts of recieved data. So to use all it we should duplicate the UIController sence in some separated place and it won't be an easy task for anyone. For example, let's imagine we need to use LexicalQuery for getting data for Inflection-games, I have as an input some word (may be with additional defining data) and want to get from LQ inflection-tables and lemmas with part of speech and definitions. I should pass:
And I should get out some data - may be it should be independent from alpheios-components or alpheios-inflection-tables - so I would be able to handle with it having only documented properties and methods. It seems to me that I will need to recreate a big part of UIController and also I will need to know a lot of details how to create it. In this way I would prefer to use some ready UIController solution but configurable and independent from any view components with well documented input and output. This way it could have an easier integration to different tasks (browser, embedded, node version). |
I think that bus events solution is a good way to use (I saw the big Kyle Simpsons speech about using events to build comunication). But it needs some additional solution for unifying events. |
Agree with you that there is no standard event handling mechanism we can use. That's why I was thinking we can create our own based on the Reactor pattern. This is pretty simple, and we won't be dependent on any platform-specific implementations. Here is one of the examples of how this can be done: https://stackoverflow.com/questions/15308371/custom-events-model-without-using-dom-events-in-javascript You've raised some very good points about LQ and UIC roles in the application. I need to think about it, and we'll write my opinion on this later. |
I like the event-based approach, and agree that, with some care, we can use a solution like the Reactor pattern so that LexicalQuery can be used without DOM events. As Irina mentioned, it's a little difficult to know where the line is between the UIController business logic and the LexicalQuery business logic. We want consistent behavior of lookups, particularly with regard to language-specific variations and user preferences (See alpheios-project/components#269 for an example of a language-specific refinement that is needed). I think UIController should be responsible for business logic of the visual components and state of the application, and the LexicalQuery should be responsible for business logic of the queries (in fact, it might be better renamed QueryController) and maybe that distinction will help? |
I agree that LexicalQuery (and QueryControllers) shall be responsible for obtaining data only. That data shall be displayed by a UIController, which, in turn, would be responsible for presentation. In that schema, a LexicalQuery should not generate any messages that will be displayed in the UI nor it should decide whether to do something on the UI side. That's responsibility of a UI Controller. A LexicalQuery should just obtain data correctly and once each new piece of data become available, it should pass that data piece to a UI Controller. Maybe it would even make sense to move Queries to the new architecture of client adapters (alpheios-project/components#264). Their purpose is to receive data from external sources, and that's what client adapters are for. I like the notion of a QueryController, because that's what current implementation of LexicalQuery is. It makes several atomic requests (to morph analyzer service, to lemma translation, etc.) and then combine data from them, making some decisions that depend on if and what data has been returned. So I think we could have two levels of objects in client adapters library: basic (atomic, or adapter-level) queries and more complex composite queries (being instances of QueryController probably). An atomic query would just send data to the remote service and get data back, without any business logic for data processing, except for parsing. A composite query would analyze data received from an atomic query, process it, if necessary, send other atomic queries based on results of data processing, and notify event subscribers (i.e. a UIController) when a new piece of data would become available. In this architecture a QueryController would to atomic queries be something that UIController is to Vue components. What do you think of such approach in general? |
that sounds promising. I kind of like the idea of moving the QueryController to the adapters library, particularly now that the adapters are combined. Need to think about it a little bit more. |
@balmas, @irina060981: In this schema, Query uses events to pass data to a UI Controller and thus is fully decoupled from it. If this approach is OK, we can go ahead with the change and decouple Query from UI controller. Then we'll be free to move it to client adapters library or leave it where it is. Query would be an independent entity from now on. I think this step is important now as it will allow us to refactor UI controller more freely as almost nothing will depend on its internal structure. That will provide a much more freedom in what we can do. |
I have some questions May be we should produce only one event type - "update data" with some delta |
I think the event-based workflow is good. I agree with @irina060981 that we should put some thought to how the events are named and whether these are all 1 type of event, distinguished by the data. I think we will probably want to move more to the event-based model, such as handling of updates to user preferences, so we might want to be careful about how many distinct event types we create. |
I'm not completely sure what the best way of implement this would be, but so far the idea is:
|
I think here it is not much difference really - I think it will have more flexibility - and we will be able to filter data more easier - what do you tnink, @kirlat ? |
We need also to think about how errors are sent and handled. We have some improvements to do here (see alpheios-project/components#277). |
@irina060981, do I understand correctly that you suggest to have only one type of event listener callback (updateData) that will be called every time some information is retrieved from LexicalQuery? I agree that it's simpler to register one callback than several, but I'm not happy that in that case we have to do routing within a UI controller. Let's compare two variants (let's say we have four events, A through D, and four methods to handle each, but we might have more):
that's nice and short, but then we have to do the routing like:
that's more tedious and repetitive, on my opinion, and is harder to read.
and that's it, we don't need the second part. I think specialized callbacks are more concise and more elegant. I also like the idea of removing routing code from the UI controller as it is, technically speaking, an auxiliary functionality to it. I would really love to have UI controller simpler. I think it's overburdened with convoluted logic now. Are your concerns are that with specialized callbacks we have to know all event callback types and if we don't we would skip some data update? I see this as an advantage. If we skip an event we don't know (and don't care about too), that's fine. It's good to isolate events that we know about (and know how to process its data) from the ones that are unknown to us. LexicalQuery is used by UIController only now, but it might be used by some other client (Games? something else?). For the second client a LexicalQuery may produce events and data formats that UIController knows nothing about (and don't need them). But those would still go into the UIController universal callback (as universal controller be called for any type of event and event data), even though UIController don't need this data and does not know how to process that. That will result in unnecessary callback calls and also, in theory, as some unknown data would sneak into a universal callback this may mess up our processing logic. I think it's better to avoid that. Might be very hard to debug (being there, done that 🙂). I think it's better to have isolated specialized callback functions. They are easier to mock and to test. What do you think? Do I understand your reasons correctly? In fact, we could combine two approaches, if we need, because we could register both specialized and universal callbacks at the same time, if we have a reason for that. |
@balmas, @irina060981: I've added a preliminary UI Controller architecture: https://github.com/alpheios-project/documentation/blob/master/development/app-architecture.md#ui-controller-architecture. Please let me know what do you think. |
I think you make a good argument about the event handlers, @kirlat . Particularly this:
The architecture diagram also looks fine to me. I'm still a little uneasy about increasing our dependency on Vue so if there is a way to abstract the Vuex store a bit, so that if we ended up swapping it out for something else we could minimize the changes I would be more comfortable, but I don't think it's worth a lot of extra work. I'd like just always to be careful when we introduce a new 3rd party dependency. |
@kirlat , may be you are right. And I am not against it because it could be re-changed if it won't be efficient.
It seems to me that we will to create switch-case parsing the event data anyway. |
@irina060981! Thanks for your comments, you're rising some very important points! They made me think about things I would've not think otherwise (especially on a design stage) and gave me some ideas to solutions that, I think, would benefit us. Here is how, on my opinion, we could handle some of the situations you've described.
I think as we are not going to create a global event loops (when an event is visible to all objects within an application) that should be less of our concerns. We could use a subscription model, where a subscriber (i.e. a UI Controller) would subscribe to listen events of a particular object class only (i.e. a LexicalQuery) like Through a subscription model the names of the events are localized within classes that fires them. It's like class names being namespaces for that events: "evntA" fired by LexicalQuery would be separated from "evntA" fired by ResourceQuery because they will have different listeners. We just need to care so there there will be no two events with the same name within an originator class (i.e. LexicalQuery), but that's easy to do. In fact, the event handling implementation will prevent that (as event names would probably be used as map keys). In fact, we can signify the naming convention described above by establishing a rule of adding originator name in front of an event name, so we could have
That's very important, but I think we can create some safety mechanisms to prevent that. Let's say we have But we can add a safety check to
If it will be better to use mini generic events, we can use those, I think, They just should really try to stay on the "mini" and be specialized. Such code is better structured and easier to manage. We don't need to force one way or the other. The callback structure would allow as to combine mini generic events with smaller specialized ones. The architecture is flexible enough to handle that.
I was thinking to create some generic event-handling classes as we did with a messenger service. Queries can then either inherit from them or include them with composition. So there would be no duplication of an event-handling code within each query class. This can still be generic if we use generic dictionaries to handle routing. We can create a map were keys would be event names and values would be arrays of callback functions. So:
I think that's pretty compact and generic. As for error handling I did not have time to think about that yet. The simplest solution would probably be to return a status code in the data object (one code for success, another one for data not found, yet another one for error, etc.) and check for that code within a callback. I understand the drawbacks of this approach (hard to maintain and sync list of codes between two parties, need a special knowledge of what each code mean), but we could probably mitigate that by providing methods for checking status of data within LexicalQuery (where those codes are generated). |
I think this is a good idea. I suggest maybe also that we use the Alpheios namespace for all Alpheios events. |
This is a debate that programmers have been having since the beginning of time, but I think I would rather use Error classes and inheritance than error codes. I think we can probably identify a few meta classes of errors that can be used as needed, and further specialized when necessary by deriving from them, being sure not to duplicate unnecessarily the core javascript error types (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) . So, just off the top of my head we might have AlpheiosServiceTimeoutError, AlpheiosDataNotFound, AlpheiosServiceException and then the adapter classes can be responsible for translating service or request specific errors into these more general classes, wrapping the original error when necessary. How does that approach sound to both of you? |
Great suggestion. Then, I think, we could have it like I also like the idea of passing instances of Error into callbacks. We could integrate error reporting into an Error class and not worry about what error message should be shown by the UI Controller if an error occurred (and not to change it in several places if we decide to change it). If UI controller wants to report an error, it can just use a method of an Error instance to get that. We could integrate translations in there too. But I think error codes could be important still for user reporting. It would probably easier for user to memorize and report: a number than an error name (or would it? What would be simpler: "58017" or "AlpheiosLexicalQueryTimeout"? I think probably the number is. People are used to memorizing or recording numbers more then they are to EXACT messages, I think). Based on the answer we might consider adding codes for the reporting purpose. Also, using codes in logs would spare us some space too. The only question I have whether we would treat "DataNotFound" as an error. Technically, it is not an error, but a normal execution condition, so it would probably be wrong to treat it as an error. Maybe make an |
@kirlat , It seems to me that you were talking about to use some custom event's solution (as described in the stackoverflow example). About using the events - If I understood correctly you suggest to use it this way: import the LexicalQuery module I am not sure here if to be honestly, because I used before some already created in global scope for listening to events (window, browser, node events). What do you think? |
@irina060981: Yes, I was thinking about a custom solution. The reason for this is that custom solution is so simple, we'll create it faster than we would integrate some more complex (and powerful) solutions. You're right that we have to keep data in global scope (in fact, it is module scope, so we're safe from collisions). However, a simple event emitter will not take much memory at all. All it takes is to have a map that will store event names and callback references. That's nothing comparing with all the lexical data we have to keep. Events in the solution I was thinking about are kind of global, but I think it would make sense for us to localize them. I created a working concept of what I had in mind (because talk is cheap 🙂). We can test it, evaluate, and decide whether it'll be a good on for us. The whole thing is less than 50 lines long: https://github.com/alpheios-project/components/blob/ui-controller-refactoring/src/lib/events/event-emitter.mjs To test it, check out a It works in any environment because it's not environment dependent at all. Solution that you provided a link for looks great and very iteresting, and there are some other good solutions too. I agree that it's better not to reinvent the wheel. However, when solution is as simple as above, I'm wondering if we really need anything more complex than that. Comparing third-party vs in-house solutions, I think the following is true. Please add your thought and correct me if I'm wrong. Third-party pros: very powerful and feature rich (but probably we won't use most of them), well optimized code, well tested, constantly (hopefully) developed so we might expect some new features Third-party cons: one more external dependency. This means increased build size because it has functionality we'll probably not use. If project be abandoned, we're in trouble. It's also harder and slower to fix issues as we have to submit them to upstream for that. From my experience, if we need something really simple, as a couple of utility functions, we better write and maintain them ourselves. If we need something that take us a lot of time to write (as Node.js has its own global events. Browsers has global DOM events too. My idea was that we don't need anything as global as that, but rather a simple solution to send events between couple of objects. However, if we would decide on complex event based architecture where all components would send events to each other, then probably we need something more complex than that. I think ideally we should use something that fulfill our needs and won't burden us with functionality we don't care about. So the main question, probably, is what do we need from an event emitter? Then it will be much simpler to find a solution to satisfy our requirements. @balmas, what do you think about that? |
@kirlat , if you don't think that there is a need in an existed solution you could use your own of course. And if there exists such need - it will become clear. |
I think all uses of 3rd party libraries need careful consideration. It's always a balance between not reinventing the wheel and becoming too dependent on code that isn't fully under our control and which might itself introduce long chains of dependencies. That said, I did take a quick look at eventemitter3 and it looks like a quite clean, well-commented piece of code that doesn't introduce any other dependencies. However it is also written in EMCA script 3 not 6, and so that is something to consider too, whether at some point that might be problematic. We have a lot to do and I don't want to get too bogged down in this choice. @kirlat your solution also looks like it might be enough to get started on this, but I would want to see unit tests added for it from the start. I do agree with @irina060981 that a well-tested solution has value. |
Splited discussion of communication protocol between components into a separate issue: #6 |
this has largely been implemented. |
@balmas, @irina060981: Please review a proposition of an application architecture for our business classes: https://github.com/alpheios-project/documentation/blob/master/development/component-architecture.md.
I think it would be beneficial to us to standardize behavior of such components as UIController, ContentProcess, BackgroundProcess, etc.
Decided to assemble this piece and offer if for discussion before doing changes to the controller objects required to avoid preliminary data loading in Safari tabs, when content script is loaded to every tab automatically.
Please let me know your opinions. It would be great if we combine our experiences and ideas on this! Thanks!
The text was updated successfully, but these errors were encountered: