samedi 30 janvier 2016

Who and how should use a global service in response to a GUI event

There are many client-side frameworks which encourage the developer to split the GUI into a tree of components. In this question I do not assume any particular framework, or even language, but rather seek for lessons learned from all the diverse frameworks like React, Vue, Ember, Aurelia, or Windows Forms or XAML, that could be useful for someone who'd like to maintain a single page app in Backbone.js.

To make the question concrete, let's think about something like facebook's main page. I have one component (ListOfPostsComponent) which displays a list of posts, where each post is actually displayed by a child component (PostComponent) which in turn might have some component for displaying comments (ListOfCommentsComponent) in which there are multiple instances of CommentComponent, and an instance of AddCommentComponent which lets the user add a comment if she types in text into NewCommentTextComponent and presses the AddCommentButtonComponent.

The goal is to POST a request trough API, on success add the comment to the list of comments, and update the number of comments displayed in the post summary. Realistic enough?

Now, it seems obvious that the click into the button triggers some chain of events starting in AddCommentButtonComponent. What follows is the interesting part. I see several different ways to approach the two problems:

Problem 1. which component should take action of initiating the POST Problem 2. how exactly it should initiate it

Here are several ideas I considered, but I invite you to propose others:

For problem 1:

Option A) (silly) the AddCommentButtonComponent is in charge of adding the comment, so it performs the request. This option requires the button to know the content of the NewCommentTextComponent and that it will inform somehow it's parents about the success of the operation. Seems like too much coupling.

Option B) the AddCommentComponent which knows about both NewCommentTextComponent content and listens to AddCommentButtonComponent 'click' event performs the request. It requires at the minimum a contract which specifies how the text content is being exposed from NewCommentTextComponent, how to listen for events and what is the event name, but it seems to be a right level of coupling, and/or reason for existence of the AddCommentComponent in the first place. We still need to fire some event to inform parents that a new comment is available.

Option C) there is some global events bus/hub, and the communication between components goes by passing one-way events, probably namespaced somehow to avoid collisions. AddCommentButtonComponent sends a 'click' event to a channel/namespace for which the AddCommentComponent listens/subscribes, which in turn emits event 'i-am-about-to-create-a-comment-please-send-me-data', for which NewCommentTextComponent responds with something, etc. Seems like a total mess. Let's call it Erlang style?

Option D) the event is propagated upwards until some authority is sure that noone above it knows what to do with it. In our example the PostComponent decides that it is the last outpost on the border between interested and not interested controls. PostComponent is interested in the event because it wants to update the comments count, and ListOfPostComponents is not interested, so we have to handle it here. This solution seems to asssume a lot of knowledge about the context: PostComponent must know who is the parent and what is it interested in, and so does AddCommentComponent.

Option E) (let's call it the HTML way) the (properly namespaced) event bubbles up to the root accumulating any additional pieces of information as it goes up. For example the AddCommentButtonComponent emits just a simple 'click', but the AddCommentComponent adds a new field with text content to it (and changes name of the event to 'add-comment'), then PostComponent decides to initiate the request but it does not stop event bubling, but might enhance the event with additional name/properties. The problem with this is that it does not answer the question: who should perform the request.

Option F) similar to Option E) but request is issued by some separate service which simply listens to events at the root.

For problem 2:

Option A) (silly) the control simply uses $.ajax.

Option B) (global app) the control uses app.api.addComment(post,text,callbacks)

Option C) (deps injection) the control uses this.api.addComment(post,text,callbacks) where the this.api is initialized in constructor and passed from the parent. The problem with this is that now the parent has to know about all the dependencies of children, and grand children etc.

Option D) (dependency injector) the control uses this.api.addComment(post,text,callbacks) where the this.api is magically initialized during construction by some dependency injector which knows what the control needs

Option E) (service locator) the control uses something like serviceLocator.getApi().addComment(post,text,callbacks).. which is really just app.api.addComment(post,text,callbacks) in disguise ?

Option F) (my interpretation of react) the control expresses intent of adding a post, but does not pass any callbacks or otherwise expect the answer. Someone delivers this intent to a service which can process it. Later when the answer comes it is pushed top-down from the root to all nodes of the tree of components (which allows for a parent to selectively filter information passed to children). Alternatively the response is passed to all components directly. This seems to me quite similar to ...

Option G) (events bus) the control emits an event that it wants a POST to be performed. A service listens to such events and performs the POST, and when it is done it triggers an event containing the answer. All components interested in answer simply subscribe to this global event.




Aucun commentaire:

Enregistrer un commentaire