📜 ⬆️ ⬇️

MVC on pure javascript

Design patterns are often embedded in popular frameworks. For example, the MVC pattern (Model-View-Controller, Model-View-Controller) can be found literally everywhere. In JavaScript, it is difficult to separate the framework from the design pattern implemented in it, and, often, the authors of the framework interpret MVC in their own way and impose their vision on the programmers.



How exactly a particular MVC implementation will look like depends entirely on the framework. As a result, we get a lot of different implementations, which is confusing and leads to confusion. This is especially noticeable when several frameworks are used in one project. This situation made me wonder: “Is there a better way?”.

The MVC pattern is good for client frameworks, however, relying on something “modern”, you need to remember that tomorrow something new will appear, and what is modern today will become obsolete. This is also a problem, and I would like to explore alternatives to frameworks and see what all this can lead to.
')
The MVC design pattern appeared a few decades ago. I suppose it’s worth investing time in any programmer. This template can be used without binding to any frameworks.

Is MVC implementation another framework?


For a start, I would like to dispel one common myth when equating design patterns and frameworks. These are different things.

A template is a rigorous approach to solving a problem. To implement the template, for which the programmer is responsible for the details, you need a certain level of skill. The MVC pattern allows you to follow the principle of separation of responsibilities and promotes the writing of clean code.

The frameworks are not tied to specific design patterns. In order to distinguish the framework from the template, you can use the so-called Hollywood principle: “do not call us, we will call you”. If there is a certain dependency in the system and at the same time you are forced to use it in a certain situation - this is a framework. The similarity of frameworks with Hollywood lies in the fact that the developers who use them are like actors who are forced to strictly follow movie scripts. Such programmers do not have the right to vote.

Should I avoid client frameworks? Everyone will answer this question himself, however, here are some good reasons to refuse them:


MVC pattern


The MVC design pattern hails from the 1970s. He appeared at the Xerox PARC Research Center while working on the Smalltalk programming language. The template has passed the test of time in the development of graphical user interfaces. He came to web programming from desktop applications and proved to be effective in a new field of application.

In essence, MVC is a way of clearly dividing responsibilities. As a result, the design of the solution based on it is clear even to a new programmer who, for some reason, joined the project. As a result, even to someone who was not familiar with the project, it is easy to understand it, and, if necessary, to contribute to its development.

MVC implementation example


To make it more fun, write a web application that will be devoted to penguins. By the way, these cute creatures, similar to plush toys, live not only in the Antarctic ice. In total there are about a dozen species of penguins. It's time to look at them. Namely, our application will consist of a single web page, on which the penguin information viewing area is located and a couple of buttons that allow you to view the penguin catalog.

When creating an application, we will use the MVC template, strictly following its principles. In addition, the process of solving the problem will involve the methodology of extremal programming , as well as unit tests. Everything will be done in JS, HTML and CSS - no frameworks, nothing more.

Having mastered this example, you will learn enough to integrate MVC into your own projects. In addition, since software built using this template is perfectly testable, you will also learn about unit tests for our training project. Understandably, if necessary, you can also adopt them.

We will adhere to the ES5 standard for cross-browser compatibility. We believe that the MVC pattern is well deserved in order for its implementation to use the well-known, proven features of the language.

So let's get started.

General project overview


The demo project will consist of three main components: the controller, the view and the model. Each of them has its own area of ​​responsibility and is focused on solving a specific task.

This is how it looks like a schematic.


Project outline

The PenguinController controller PenguinController event handling and serves as an intermediary between the view and the model. He finds out what happened when the user performs an action (for example, clicks a button or presses a key on the keyboard). Client application logic can be implemented in the controller. In larger systems in which you need to handle a lot of events, this element can be divided into several modules. The controller is the entry point for events and the only mediator between the view and the data.

The PenguinView interacts with the DOM. DOM is the browser API that is used to work with HTML. In MVC, only the view is responsible for changing the DOM. A view can connect UI event handlers, but event handling is the controller's prerogative. The main task solved by the presentation is to control what the user sees on the screen. In our project, the view will perform manipulations with the DOM using JavaScript.

The PenguinModel model PenguinModel responsible for working with data. In client JS, this means performing Ajax operations. One of the advantages of the MVC pattern is that all interaction with the data source, for example - with the server, is concentrated in one place. This approach helps programmers who are not familiar with the project understand it. The model in this design pattern is engaged exclusively in working with JSON or objects that come from the server.

If the implementation of MVC violates the above division of responsibilities of components, we get one of the possible MVC anti-patterns. The model should not work with HTML. A view should not perform Ajax requests. The controller should play the role of an intermediary, not caring about the details of the implementation of other components.

If the web developer, when implementing MVC, does not pay enough attention to the division of responsibility of components, everything, as a result, turns into one web component. As a result, despite the good intentions, it turns out a mess. This is due to the fact that increased attention is paid to the capabilities of the application and all that is associated with user interaction. However, the division of responsibilities of components in the areas of program capabilities is not the same as the division of functions.

I think in programming it is worth striving for a clear separation of functional areas of responsibility. As a result, each task is provided with a uniform way to solve it. This makes the code more understandable, allows new programmers connecting to the project to more quickly understand the code and begin to work on it productively.

Perhaps enough reasoning, it's time to look at the working example, the code of which is posted on CodePen . You can experiment with it.


CodePen application

Consider this code.

Controller


The controller interacts with the view and model. The components necessary for the operation of the controller are in its constructor:

 var PenguinController = function PenguinController(penguinView, penguinModel) { this.penguinView = penguinView; this.penguinModel = penguinModel; }; 

The constructor uses inversion control , modules are embedded in it in accordance with this idea. Inversion control allows you to embed any components that meet certain high-level contracts. This can be considered as a convenient way to abstract from implementation details. This approach helps to write clean code in javascript.

Then events related to user interaction are activated:

 PenguinController.prototype.initialize = function initialize() { this.penguinView.onClickGetPenguin = this.onClickGetPenguin.bind(this); }; PenguinController.prototype.onClickGetPenguin = function onClickGetPenguin(e) { var target = e.currentTarget; var index = parseInt(target.dataset.penguinIndex, 10); this.penguinModel.getPenguin(index, this.showPenguin.bind(this)); }; 

Note that event handlers use application state data stored in the DOM. In this case, this information is enough for us. The current state of the DOM is what the user sees in the browser. Application state data can be stored directly in the DOM, but the controller should not independently affect the state of the application.

When an event occurs, the controller reads the data and makes decisions about further actions. At the moment we are talking about the callback function this.showPenguin() :

 PenguinController.prototype.showPenguin = function showPenguin(penguinModelData) { var penguinViewModel = {   name: penguinModelData.name,   imageUrl: penguinModelData.imageUrl,   size: penguinModelData.size,   favoriteFood: penguinModelData.favoriteFood }; penguinViewModel.previousIndex = penguinModelData.index - 1; penguinViewModel.nextIndex = penguinModelData.index + 1; if (penguinModelData.index === 0) {   penguinViewModel.previousIndex = penguinModelData.count - 1; } if (penguinModelData.index === penguinModelData.count - 1) {   penguinViewModel.nextIndex = 0; } this.penguinView.render(penguinViewModel); }; 

The controller, based on the state of the application and on the event that occurred, finds the index corresponding to the data set about the penguin, and informs the presentation about what needs to be displayed on the page. The controller takes the necessary data from the model and converts it into an object with which the view can work.

The unit tests presented here are based on the AAA model (Arrange, Act, Assert - placement, action, approval). Here is the unit test for the standard penguin information script:

 var PenguinViewMock = function PenguinViewMock() { this.calledRenderWith = null; }; PenguinViewMock.prototype.render = function render(penguinViewModel) { this.calledRenderWith = penguinViewModel; }; // Arrange var penguinViewMock = new PenguinViewMock(); var controller = new PenguinController(penguinViewMock, null); var penguinModelData = { name: 'Chinstrap', imageUrl: 'http://chinstrapl.jpg', size: '5.0kg (m), 4.8kg (f)', favoriteFood: 'krill', index: 2, count: 5 }; // Act controller.showPenguin(penguinModelData); // Assert assert.strictEqual(penguinViewMock.calledRenderWith.name, 'Chinstrap'); assert.strictEqual(penguinViewMock.calledRenderWith.imageUrl, 'http://chinstrapl.jpg'); assert.strictEqual(penguinViewMock.calledRenderWith.size, '5.0kg (m), 4.8kg (f)'); assert.strictEqual(penguinViewMock.calledRenderWith.favoriteFood, 'krill'); assert.strictEqual(penguinViewMock.calledRenderWith.previousIndex, 1); assert.strictEqual(penguinViewMock.calledRenderWith.nextIndex, 3); 

The stub PenguinViewMock implements the same contract as the actual view module. This allows you to write unit tests and check, in the Assert block, whether everything works as it should.

The assert object is taken from Node.js , but you can use a similar object from the Chai library. This allows you to write tests that can be performed both on the server and in the browser.

Note that the controller does not care about the implementation details. It relies on a contract that provides a view, like this.render() . It is this approach that you must follow to write clean code. The controller, with this approach, can entrust the component with the execution of those tasks that this component has declared possible. This makes the project structure transparent, which improves the readability of the code.

Representation


A view cares only about DOM elements and connecting event handlers. For example:

 var PenguinView = function PenguinView(element) { this.element = element; this.onClickGetPenguin = null; }; 

Here's how the impact of the view on what the user sees is implemented in the code:

 PenguinView.prototype.render = function render(viewModel) { this.element.innerHTML = '<h3>' + viewModel.name + '</h3>' +   '<img class="penguin-image" src="' + viewModel.imageUrl +     '" alt="' + viewModel.name + '" />' +   '<p><b>Size:</b> ' + viewModel.size + '</p>' +   '<p><b>Favorite food:</b> ' + viewModel.favoriteFood + '</p>' +   '<a id="previousPenguin" class="previous button" href="javascript:void(0);"' +     ' data-penguin-index="' + viewModel.previousIndex + '">Previous</a> ' +   '<a id="nextPenguin" class="next button" href="javascript:void(0);"' +     ' data-penguin-index="' + viewModel.nextIndex + '">Next</a>'; this.previousIndex = viewModel.previousIndex; this.nextIndex = viewModel.nextIndex; //             var previousPenguin = this.element.querySelector('#previousPenguin'); previousPenguin.addEventListener('click', this.onClickGetPenguin); var nextPenguin = this.element.querySelector('#nextPenguin'); nextPenguin.addEventListener('click', this.onClickGetPenguin); nextPenguin.focus(); } 

Note that the main task of the view is to turn the data obtained from the model into HTML and change the state of the application. Another task is to connect event handlers and transfer functions to the controller. Event handlers are connected to the DOM after the state changes. This approach allows you to easily and conveniently manage events.

In order to test all this, we can check the update of the elements and the state change of the application:

 var ElementMock = function ElementMock() { this.innerHTML = null; }; // -,   ,    ElementMock.prototype.querySelector = function querySelector() { }; ElementMock.prototype.addEventListener = function addEventListener() { }; ElementMock.prototype.focus = function focus() { }; // Arrange var elementMock = new ElementMock(); var view = new PenguinView(elementMock); var viewModel = { name: 'Chinstrap', imageUrl: 'http://chinstrap1.jpg', size: '5.0kg (m), 4.8kg (f)', favoriteFood: 'krill', previousIndex: 1, nextIndex: 2 }; // Act view.render(viewModel); // Assert assert(elementMock.innerHTML.indexOf(viewModel.name) > 0); assert(elementMock.innerHTML.indexOf(viewModel.imageUrl) > 0); assert(elementMock.innerHTML.indexOf(viewModel.size) > 0); assert(elementMock.innerHTML.indexOf(viewModel.favoriteFood) > 0); assert(elementMock.innerHTML.indexOf(viewModel.previousIndex) > 0); assert(elementMock.innerHTML.indexOf(viewModel.nextIndex) > 0); 

We reviewed the view and controller. They solve most of the tasks assigned to the application. Namely, they are responsible for what the user sees and allow him to interact with the program through the event mechanism. It remains to deal with where the data is taken from, which are displayed on the screen.

Model


In the MVC pattern, the model is busy interacting with the data source. In our case - with the north. For example:

 var PenguinModel = function PenguinModel(XMLHttpRequest) { this.XMLHttpRequest = XMLHttpRequest; }; 

Note that the XMLHttpRequest module is embedded in the model constructor. This is, among other things, a hint for other programmers regarding the components needed by the model. If a model needs different ways of working with data, other modules can be embedded in it. As in the cases discussed above, unit tests can be prepared for the model.

Get the penguin data based on the index:

 PenguinModel.prototype.getPenguin = function getPenguin(index, fn) { var oReq = new this.XMLHttpRequest(); oReq.onload = function onLoad(e) {   var ajaxResponse = JSON.parse(e.currentTarget.responseText);   //     ,        var penguin = ajaxResponse[index];   penguin.index = index;   penguin.count = ajaxResponse.length;   fn(penguin); }; oReq.open('GET', 'https://codepen.io/beautifulcoder/pen/vmOOLr.js', true); oReq.send(); }; 

Here you can connect to the server and download data from it. Check the component using the unit test and conditional test data:

 var LIST_OF_PENGUINS = '[{"name":"Emperor","imageUrl":"http://imageUrl",' + '"size":"36.7kg (m), 28.4kg (f)","favoriteFood":"fish and squid"}]'; var XMLHttpRequestMock = function XMLHttpRequestMock() { //      ,     this.onload = null; }; XMLHttpRequestMock.prototype.open = function open(method, url, async) { //  ,      method  url assert(method); assert(url); //  Ajax  ,      :-) assert.strictEqual(async, true); }; XMLHttpRequestMock.prototype.send = function send() { //     Ajax- this.onload({ currentTarget: { responseText: LIST_OF_PENGUINS } }); }; // Arrange var penguinModel = new PenguinModel(XMLHttpRequestMock); // Act penguinModel.getPenguin(0, function onPenguinData(penguinData) { // Assert assert.strictEqual(penguinData.name, 'Emperor'); assert(penguinData.imageUrl); assert.strictEqual(penguinData.size, '36.7kg (m), 28.4kg (f)'); assert.strictEqual(penguinData.favoriteFood, 'fish and squid'); assert.strictEqual(penguinData.index, 0); assert.strictEqual(penguinData.count, 1); }); 

As you can see, the model is only concerned with the raw data. This means working with Ajax and with JavaScript objects. If you don’t fully own the Ajax theme in javascript, here’s some useful stuff .

Unit Tests


With any rules for writing code, it is important to check what happened. The MVC design pattern does not regulate how to solve the problem. Within the framework of the template, the boundaries are quite free, without crossing which you can write clean code. This gives freedom from the dominance of addictions.

I usually strive to have a complete set of unit tests covering each use case of a product. These tests can be viewed as guidelines for using code. Such an approach makes the project more open, understandable for any programmer who wants to participate in its development.

Experiment with a complete set of unit tests. They will help you better understand the design pattern described here. Each test is designed to test a specific use case of the program. Unit tests will help to clearly separate the tasks, and, when implementing a project’s functionality, not to be distracted by its other parts.

About the development of the educational project


Our project implements only the basic functionality necessary to demonstrate the implementation of MVC. However, it can be expanded. For example, here are some of the possible improvements:


If you want to develop this project as an exercise, keep in mind that the above are just a few ideas. You can easily, with the use of MVC, implement other functionality.

Results


We hope you appreciate the advantages that the MVC design pattern and disciplined approach to the architecture and project code provide. A good design pattern, on the one hand, does not interfere with work, on the other hand, it contributes to the writing of clean code. He, in the process of solving a problem, does not allow you to turn off the right path. This increases the efficiency of the programmer.

Programming is the art of sequential problem solving, splitting the system into small parts that implement a certain functional. In MVC, this means strict adherence to the principle of separation of the areas of functional responsibility of the components.

Developers usually believe that they are not amenable to the action of emotions, that their actions are always logical. However, when trying to simultaneously solve several problems, a person finds himself in a difficult psychological situation. The load is too high. Working in such a state badly affects the quality of the code. At a certain point, the logic fades into the background, as a result, the risk of errors increases, the quality of the code deteriorates.

That is why it is recommended to split projects into small tasks and solve them in turn. A disciplined approach to MVC implementation and the preparation of unit tests for various parts of the system contribute to calm and productive work.

Dear readers! What design patterns do you use in your JS projects?

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


All Articles