📜 ⬆️ ⬇️

And again about MVC

I found very few MVC implementations for JavaScript on the network, so I would like to reveal the advantages of this approach in the implementation of JavaScript.

I will say in advance that the use of MVC is more suitable for business applications, games, interfaces such as admins or something like Gmail / Google docs. If you have a promo site with a thousand banners and flying snowflakes across the screen, using MVC is pointless and even harmful.

A bit of history:
MVC (Model View Controller) is described in 1979, for the SmallTalk language.
Since then, nothing new in the interfaces has been invented. The template improved, supplemented and expanded. Followers appeared depending on the capabilities of the platform and the language in which the application is written, such as HMVC (Hierarchical Model View Controller), MVP (Model View Presenter), MVVM (Model View ViewModel), but the essence remains the same - separating the data source from the modifying and commanding functions and their weak binding.
I will say in advance that from the very appearance of the template in 1979, the first review was like this: " Why is it necessary? And so everything works well. ", The second review: " Bah! Yes, the data is duplicated in the View and in the Model. Everything will slow down! "and, finally, the third:" With each change of the model, the view is updated. Everything will slow down! ". So do not be surprised that when you read this article, you will have similar questions. Despite everything, MVC and its followers have become very successful and time-tested solutions for quite a few tasks. I hope you see this too.
')
The task : to create a simple interactive page - a list of values, and two buttons - to add and delete values. The traditional approach is quite simple - we create an html file. Select is used for the list, button is used for manipulations. In the onClick attribute of the buttons, we register the call to the JavaScript function. To remove a value from select, we refer to the select DOM object, ask the selectedIndex property, and delete the corresponding object.
<html><head><script> // <![CDATA[ function addItem (value) { if (!value) { return; } var myselect = document.getElementById('myselect'); var newoption = myselect.appendChild(document.createElement('option')); newoption.value = value; newoption.innerHTML=value; } function removeCurrentItem () { var myselect = document.getElementById('myselect'); if (myselect.selectedIndex === -1) { return; } var selectedOption = myselect.options[myselect.selectedIndex]; selectedOption.parentNode.removeChild(selectedOption); } // ]]> </script> </head> <body> <select id="myselect" size="4"></select> <button onClick="addItem(prompt('enter value'))">+</button> <button onClick="removeCurrentItem()" />-</button> </body> </html> 

Everything is simple and works fine. Why do you need something else? For this application, problems are not expected, however, according to statistics, in fact, writing code takes about 25% of the time, and the remaining 75% of the programmer is engaged in the development, support of the project, adding new functions. And as the complexity of the application and the increase in the amount of support for the code becomes more and more difficult, the number of bugs increases.

“I will do it through jQuery / Dojo / MooTools / My_loved_Aykes_ Library”, you say. For example:
 <html><head> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script> <script> // <![CDATA[ $(document).ready(function(){ $('#plus').bind('click', function(){ var value = prompt('add value'); if (!value) { return; } $('<option>').html(value).appendTo($('#myselect')); }); $('#minus').bind('click', function(){ var myselect = $('#myselect'); if (myselect.attr('selectedIndex') === -1) { return; } myselect.children().remove(':selected'); }); }); // ]]> </script> </head> <body> <select id="myselect" size="4"></select> <button id="plus">+</button> <button id="minus"/>-</button> </body> </html> 

It became even better, the code became more compact and it looks very cool. However, this did not solve the main problem: the data (list) is stored inside the user interface, inside the html of the select element. The second drawback is that we have a jumble of html and javascript code. Despite its simplicity, it develops poorly. For example, the boss comes up to us and says the simplest request: “I want the minus button to appear only when something is selected in the list.”. Immediately you, as a programmer, have a question: where to make this change - in html or in JavaScript. “Hmm, I’ll probably turn off the button in html” - you will say. And your colleague, while you are on vacation, will perform a similar operation inside JavaScript.
It turns out that a seemingly simple change starts to confuse the code very much, I would like to avoid this.

In the classic MVC implementation, the basic principles are:
  1. Loose binding
  2. The model knows nothing about anyone. The model sends out alerts that can be heard, for example, Kind, if it wants.
  3. The view knows about the model but cannot change it; the view can manipulate the controller
  4. The controller knows about the Model and can change it, and also knows about the View (or Views) and can change it (them).

In this case, the differences are as follows:
  1. The model knows nothing about anyone. May send alerts
  2. Kind knows only about the model
  3. The controller knows both about the View and about the Model.

As you can see, unlike the classic template, this template is more rigid, because the view knows nothing about the controller, I don’t like the idea of ​​cross linking objects (View and Controller), and for now I can try to do without such linking. Let's call it, for example, Orthodox MVC (Orthodox MVC)

Model

In our case, the model simply stores an array of objects (strings) that we entered. The sequence number of the “current” object is also stored. There are also standard methods for adding and removing objects: addItem, removeCurrentItem, you can't do without them, because Models need to send alerts about their change.
To send an alert, use a object of the project type (modelChangedSubject), you can make several such objects and send to subscribers not just the “model has changed” notification, but “the model's X property has changed”. This will help the View to be updated not entirely but only to those sites that have really changed in the model.
I didn’t find the standard subdzekt creation function in jquery, so I took the ready makeObservableSubject from a great mvc article by a Canadian programmer .

 ... OMVC.Model = function () { var that = this; var items = []; this.modelChangedSubject = OMVC.makeObservableSubject(); this.addItem = function (value) { if (!value) { return; } items.push(value); that.modelChangedSubject.notifyObservers(); }; this.removeCurrentItem = function () { if (that.selectedIndex === -1) { return; } items.splice(that.selectedIndex, 1); that.modelChangedSubject.notifyObservers(); }; this.getItems = function () { return items; }; this.selectedIndex = -1; this.getSelectedIndex = function () { return that.selectedIndex; } this.setSelectedIndex = function (value) { that.selectedIndex = value; that.modelChangedSubject.notifyObservers(); } }; ... 


View

As you can see, our html body is “empty”, all elements are drawn “manually” i.e. using javascript. Check out Gmail - it's the same there. All elements: buttons, lists are drawn with javascript.
The view knows about the model, but ideally the View should have access to the model in read-only mode, i.e. you can not allow the mind to call removeCurrentItem even if you really want it.
Unfortunately, I have not yet figured out how to allow some functions in JavaScript to allow changes to another object, and leave other functions only in read mode. If you have any idea how to achieve this, please share it.
 ... OMVC.View = function (model, rootObject) { var that = this; that.select = $('<select/>').appendTo(rootObject); that.select.attr('size', '4'); that.buttonAdd = $('<button>+</button>').appendTo(rootObject).height(20); that.buttonRemove = $('<button>-</button>').appendTo(rootObject).height(20); model.modelChangedSubject.addObserver(function () { var items = model.getItems(); var innerHTML = ''; for (var i = 0; i<items.length; i += 1) { innerHTML += "<option>"+items[i]+"</option>"; } that.select.html(innerHTML); }); }; ... 


Controller

In this controller, we simply hang events on the objects of the view, linking the model and the view.
 ... OMVC.Controller = function (model, view) { view.buttonAdd.bind('click', function () { model.addItem(prompt('addvalue')); }); view.buttonRemove.bind('click', function () { model.removeCurrentItem(); }); view.select.bind('click', function () { model.setSelectedIndex(view.select[0].selectedIndex); }); }; ... 


Full text of the page:
 <!doctype html> <html> <body> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script> <script> // <![CDATA[ var OMVC = {}; OMVC.makeObservableSubject = function () { var observers = []; var addObserver = function (o) { if (typeof o !== 'function') { throw new Error('observer must be a function'); } for (var i = 0, ilen = observers.length; i < ilen; i += 1) { var observer = observers[i]; if (observer === o) { throw new Error('observer already in the list'); } } observers.push(o); }; var removeObserver = function (o) { for (var i = 0, ilen = observers.length; i < ilen; i += 1) { var observer = observers[i]; if (observer === o) { observers.splice(i, 1); return; } } throw new Error('could not find observer in list of observers'); }; var notifyObservers = function (data) { // Make a copy of observer list in case the list // is mutated during the notifications. var observersSnapshot = observers.slice(0); for (var i = 0, ilen = observersSnapshot.length; i < ilen; i += 1) { observersSnapshot[i](data); } }; return { addObserver: addObserver, removeObserver: removeObserver, notifyObservers: notifyObservers, notify: notifyObservers }; }; OMVC.Model = function () { var that = this; var items = []; this.modelChangedSubject = OMVC.makeObservableSubject(); this.addItem = function (value) { if (!value) { return; } items.push(value); that.modelChangedSubject.notifyObservers(); }; this.removeCurrentItem = function () { if (that.selectedIndex === -1) { return; } items.splice(that.selectedIndex, 1); that.modelChangedSubject.notifyObservers(); }; this.getItems = function () { return items; }; this.selectedIndex = -1; this.getSelectedIndex = function () { return that.selectedIndex; } this.setSelectedIndex = function (value) { that.selectedIndex = value; that.modelChangedSubject.notifyObservers(); } }; OMVC.View = function (model, rootObject) { var that = this; that.select = $('<select/>').appendTo(rootObject); that.select.attr('size', '4'); that.buttonAdd = $('<button>+</button>').appendTo(rootObject).height(20); that.buttonRemove = $('<button>-</button>').appendTo(rootObject).height(20); model.modelChangedSubject.addObserver(function () { var items = model.getItems(); var innerHTML = ''; for (var i = 0; i<items.length; i += 1) { innerHTML += "<option>"+items[i]+"</option>"; } that.select.html(innerHTML); }); }; OMVC.Controller = function (model, view) { view.buttonAdd.bind('click', function () { model.addItem(prompt('addvalue')); }); view.buttonRemove.bind('click', function () { model.removeCurrentItem(); }); view.select.bind('click', function () { model.setSelectedIndex(view.select[0].selectedIndex); }); }; $(document).ready(function () { var model = new OMVC.Model(); var view = new OMVC.View(model, $('<div/>').appendTo($("body"))); var controller = new OMVC.Controller(model, view); }); // ]]> </script> </body> </html> 

Conclusion

Finally, the question is : what advantages does an implementation in the form of MVC give us over the first, standard or improved jQuery-ev? We wrote three times more code and for what?
Answer : Even with such a simple example, the advantages of the MVC pattern are slowly becoming noticeable:
  1. There is no need to assign id elements to the interface elements in order to receive them later on document.getElementById and similar functions, because the links to all elements are already stored in the View, you do not need to search for them by html document.
  2. All objects are drawn inside the rootObject element. A view can be easily used many times for any rootObject.
  3. In html body nothing is stored, i.e. Of course there is something there, but it doesn’t interest us at all - before the change we needed to search and do in two places: in html and in JavaScript functions. Now everything is done only in JavaScript and moreover, the design is stored only in the View object.
  4. The changes are very easy. For example, the “chief” request mentioned above is done as follows:
    • create a subdjet to change selectedIndex
      this.selectedIndexChangedSubject = OMVC.makeObservableSubject ();
    • when changing, notify that.selectedIndexChangedSubject.notifyObservers ();
    • and in the form we subscribe to the selectedIndexChangedSubject alert

Full, final version with the addition of features backlit:
 <!doctype html> <html> <body> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script> <script> // <![CDATA[ var OMVC = {}; OMVC.makeObservableSubject = function () { var observers = []; var addObserver = function (o) { if (typeof o !== 'function') { throw new Error('observer must be a function'); } for (var i = 0, ilen = observers.length; i < ilen; i += 1) { var observer = observers[i]; if (observer === o) { throw new Error('observer already in the list'); } } observers.push(o); }; var removeObserver = function (o) { for (var i = 0, ilen = observers.length; i < ilen; i += 1) { var observer = observers[i]; if (observer === o) { observers.splice(i, 1); return; } } throw new Error('could not find observer in list of observers'); }; var notifyObservers = function (data) { // Make a copy of observer list in case the list // is mutated during the notifications. var observersSnapshot = observers.slice(0); for (var i = 0, ilen = observersSnapshot.length; i < ilen; i += 1) { observersSnapshot[i](data); } }; return { addObserver: addObserver, removeObserver: removeObserver, notifyObservers: notifyObservers, notify: notifyObservers }; }; OMVC.Model = function () { var that = this; var items = []; this.modelChangedSubject = OMVC.makeObservableSubject(); this.addItem = function (value) { if (!value) { return; } items.push(value); that.modelChangedSubject.notifyObservers(); }; this.removeCurrentItem = function () { if (that.selectedIndex === -1) { return; } items.splice(that.selectedIndex, 1); if (items.length === 0) { that.setSelectedIndex(-1); } that.modelChangedSubject.notifyObservers(); }; this.getItems = function () { return items; }; this.selectedIndex = -1; this.getSelectedIndex = function () { return that.selectedIndex; } this.selectedIndexChangedSubject = OMVC.makeObservableSubject(); this.setSelectedIndex = function (value) { that.selectedIndex = value; that.selectedIndexChangedSubject.notifyObservers(); } }; OMVC.View = function (model, rootObject) { var that = this; that.select = $('<select/>').appendTo(rootObject); that.select.attr('size', '4'); that.buttonAdd = $('<button>+</button>').appendTo(rootObject).height(20); that.buttonRemove = $('<button>-</button>').appendTo(rootObject).height(20).fadeOut(); model.modelChangedSubject.addObserver(function () { var items = model.getItems(); var innerHTML = ''; for (var i = 0; i<items.length; i += 1) { innerHTML += "<option>"+items[i]+"</option>"; } that.select.html(innerHTML); }); model.selectedIndexChangedSubject.addObserver(function () { if(model.getSelectedIndex() === -1) { that.buttonRemove.fadeOut(); } else { that.buttonRemove.fadeIn(); } }); }; OMVC.Controller = function (model, view) { view.buttonAdd.bind('click', function () { model.addItem(prompt('addvalue')); }); view.buttonRemove.bind('click', function () { model.removeCurrentItem(); }); view.select.bind('click', function () { model.setSelectedIndex(view.select[0].selectedIndex); }); }; $(document).ready(function () { var model = new OMVC.Model(); var view = new OMVC.View(model, $('<div/>').appendTo($("body"))); var controller = new OMVC.Controller(model, view); }); // ]]> </script> </body> </html> 

Previously, we had an interactive html page - and now there is a javascript application with a graphical HTML interface. Feel the difference.

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


All Articles