📜 ⬆️ ⬇️

Application of an event-driven model in a web application

The interaction of application parts with each other is an important part of the architecture of any program.
And there are many patterns for their implementation. I would like to use the example of a web application to show the application of one of them, namely the Event-driven model.
It is well known to any frontend developer — every time you work with DOM events, you use this model. Let's try to build on it is not a small web application - a file manager.



Why file manager? Because, in my opinion, this model is great for applications that need to be able to work with different sets of modules (and not only their own). Well, also because I'm currently working on a new version of our file manager, where I use it. So all the examples are quite real, although they are simplified beyond recognition.
')
So let's get started!

What does our application consist of?
  1. Core. Stores data about files and interacts with the server part.
  2. View. Draws a list of files and responds to user actions
  3. Commands. Perform certain actions with files
Now we describe how our components interact with each other.

And in order for our enlightenment to acquire the true power of Tao, we first describe the unfortunate way of interaction between the modules - “direct contact” with each other.

The kernel receives a list of files from the server side and gives it a look. The view shows the user what is contained in the current directory and is waiting for his actions.
The user clicks on the file. View appeals to the core for an explanation of what is happening. The kernel reassures the view, saying that nothing happened - just the file is selected, and it is now up to date.
The user double-clicks the folder. A panicked look at the core for help. The kernel coolly informs the mind that it has addressed the wrong address, and sends it to the "open" command ...

- Difficult?

But so far tolerated. And now add another view - directory tree. Now the kernel has to remember that the list of the received files should be transferred to this view.
And add a view - «favorites» .
The core is in shock - “is this what you need?” .

I will not continue - and it is so clear that we have no opportunity for extensions. To implement each new command, we will have to constantly build upon the kernel.

Ideology of events


And now we will meditate on the task, until as a result of enlightenment (I promised that it will come) we will not speak:

- Any change - there is an event!

And in order not to forget tomorrow what we meant, we will write down some details.
  1. The application has an object, in our case, the kernel, which accepts a subscription to events.
  2. Everyone (including the kernel) subscribes to important events for them.
  3. When an event occurs, the kernel notifies all subscribers about it.
  4. Any object can generate an event.
  5. The list of events is not limited.


Let us return to our example and see how much easier it all became.
The kernel receives a list of files from the server side and generates an "open" event. All types paint what they should.
The user clicks on the file. The view generates a select event. The kernel remembers which file is selected.
The user double-clicks the folder. The view generates the dblclick event. Without a second's delay, the “open” command rushes into battle and forces the kernel to make a request to the server.

- It became easier?

Sure. But in addition, our application has acquired two important properties.
  1. Weak connectivity Modules no longer need to know anything about each other.
  2. The second property is not so obvious, and for those who did not notice it, I will explain at the end.


And now let's move on to what we love the most - to write code!

The implementation will look the same as in jQuery, with one exception -
The data associated with a particular event call will be transmitted in the field of the event object itself.

"use strict"; //   window.elFinder = function(node, options) { var self = this, //   listeners = {}; /** *   id      * * @type Array */ this.selected = []; /** *     * * @type Object */ this.ui = {}; /** *     * * @type Object */ this.commands = {}; /** *   . * * @param String  ,          * @param Object   * @return elFinder */ this.bind = function(event, callback) { var i; if (typeof(callback) == 'function') { event = ('' + event).toLowerCase().split(/\s+/); for (i = 0; i < event.length; i++) { if (listeners[event[i]] === void(0)) { listeners[event[i]] = []; } listeners[event[i]].push(callback); } } return this; }; /** *  . * * @param String   * @param Object   * @return elFinder */ this.unbind = function(event, callback) { var l = listeners[('' + event).toLowerCase()] || [], i = l.indexOf(callback); i > -1 && l.splice(i, 1); return this; }; /** *     . * * @param String   * @param Object     * @return elFinder */ this.trigger = function(event, data) { var event = event.toLowerCase(), handlers = listeners[event] || [], i; if (handlers.length) { event = $.Event(event); for (i = 0; i < handlers.length; i++) { //           //    event.data = $.extend(true, {}, data); try { if (handlers[i](event, this) === false || event.isDefaultPrevented()) { break; } } catch (ex) { window.console && window.console.log && window.console.log(ex); } } } return this; } /** *     . * * @param Object    * @return jQuery.Deferred */ this.ajax = function(data) { var self = this, dfrd = $.Deferred() .fail(function(error) { self.error({error : error}); }) .done(function(data) { //      self.trigger(data.cmd, data); }); $.ajax({ // ...   data : data }).error(function() { dfrd.reject('Unable to connect to backend'); }).success(function(data) { if (!this.validData(data)) { dfrd.reject('Invalid data from backend'); } else if (data.error) { dfrd.reject(data.error); } dfrd.resolve(data); }) return dfrd; } //  -  - // .............. //   ""   // -  / . $.each(['open', 'select', 'error'], function(i, name) { self[name] = function() { var arg = arguments[0]; return arguments.length == 1 && typeof(arg) == 'function' ? self.bind(name, arg) : self.trigger(name, $.isPlainObject(arg) ? arg : {}); } }); //       this.open(function(e) { //     }) .select(function(e) { //     this.selected = e.data.selected; }) .error(function(e) { //     alert(e.data.error); }); } elFinder.prototype = { //  views : { //   cwd : function(fm, node) { var self = this, view = $('<div/>') .appendTo(node) .delegate('div[id]', 'click', function() { view.find('div[id]').removeClass('selected'); $(this).addClass('selected'); //      "select" fm.select({selected : [this.id]}); }) .delegate('div[id]', 'dblclick', function() { fm.trigger('dblclick', {target : this.id}); }) ; //    "open" fm.open(function(e) { var files = e.data.files; //      }); } }, //  commands : { open : function(fm) { this.enabled = false; //    "dblclick" fm.select(function(e) { // ,     / //     ,     Enter this.enabled = e.data.selected.length > 0; }) bind('dblclick', function(e) { //     fm.ajax({cmd : 'open', target : e.data.target}); }); } } } 


I repeat once again that the code is greatly simplified and significantly differs from the real code of our project. If you are curious to see what it looks like in reality - a project on github

And now let's summarize.


What benefits did we get by using the event-driven model?

  1. Weakly connected components of the application and, as a result, are good opportunities for its further expansion.
  2. Open API. This is not obvious at first, but without doing anything on purpose,
    we have created an API that will allow other developers to create extensions without interfering with the main project code. And any external script will be able to interact with our application.

Disadvantages:

  1. "Broadcast". You can not send a message to a specific listener. In our case it is not important.
  2. "Insecurity". Any object can listen to any event and any object can generate an event. In our case, the second is important. That is, students can not trust the data that is transmitted to them. It is solved by checking the data in the listener.
  3. Who is the first - that and sneakers. Any listener can stop further notification of the event. In theory, not solved in any way. Partially solved by controlling the sequence of connecting modules / subscriptions to events. But we are not the first year working with DOM events and this flaw is well known to us.

In my opinion, the only significant minus for us does not outweigh all the advantages of using this remarkable pattern.

Related Links:
en.wikipedia.org/wiki/Coupling_(computer_science)
en.wikipedia.org/wiki/Event-driven_architecture
ru.wikipedia.org/wiki/Event-oriented_programming

Source: https://habr.com/ru/post/119304/


All Articles