📜 ⬆️ ⬇️

Javascript: the path to code clarity

Working code is not always perfect, but when creating texts of programs it is worth striving to make them easy to read, understand and modify. It should strive for clarity of code. To achieve this, the code must be well organized, even before the editor is opened, everything needs to be carefully planned, to think about a justified division of tasks into program components.

image

Programming with the clarity of what happens is what separates great developers from ordinary developers. In this article, we want to give a few basic principles that will allow you to take the first steps towards a clear code.

I would immediately like to note that although the ideas considered here are applicable to a wide range of programming languages, most of the examples are written in object-oriented JavaScript. If you are not familiar with this approach to development in this language, you can see the materials on the Module pattern and on the prototype object-oriented JS programming . This will help you quickly master what we are going to talk about here.

Principle of sole duty


Imagine that you are doing household chores and have a drill in order to screw a screw into the wall. When you did the job and lowered the tool, it turned out that your drill has one interesting feature. In addition to its main function, she sprays a quick-drying compound on top of the screws fastened to it, which hides the screw, imitating plaster. This is very good if you were going to paint a section of the wall into which a screw was screwed, however, this is not always necessary. In addition, I would not want to get a second drill in order to simply drill a hole in something. A drill would be a much more useful and reliable tool if it only performed one function. This would make it quite a flexible tool, suitable for use in many situations.
')
The principle of a single duty indicates that a code fragment should do one thing, and do it well. As with the drill from the previous example, limiting the functionality actually increases the usefulness of the code fragment. Programming with this in mind, not only eliminates a lot of trouble, but also makes life easier for developers who will join the project in the future.

Consider functions and methods in terms of their area of ​​responsibility. By extending the responsibility of the code snippet, you make it less flexible and reliable, more demanding of changes and more error prone. To achieve code clarity, each function or method must solve a single problem.

If you are describing what a function should do and at the same time use the union “and”, then this function is probably too complex. The task solved by a function should be simple enough so that it can be described only with the help of the meaningful name of the function and the list of its arguments, whose names also indicate their role in the task solved by the function.

Recently, I was given the task to create an electronic version of the Myers-Briggs test. I used to have to do it before. When I, for the first time a few years ago, faced the same task, I created a huge function called processForm . She calculated the results, plotted the charts, and managed everything related to DOM and data visualization.

The problem was that if something needed to be changed, it was necessary to go through a whole mountain of code only to understand exactly where the change should be made. In addition, if something went wrong somewhere in the depths of the function, finding the error was very difficult.

So, when this time I faced the same task, I broke all the logic into functions, each of which solved a single task. I wrapped these functions in a module object. As a result, the function that was called when the form was submitted looked like this:

 return {   processForm: function() {       getScores();       calculatePercentages();       createCharts();       showResults();   } }; 

Here you can take a look at the full application code.

This code is very easy to read, understand and modify. Even a person far from programming can understand what is happening here. And each of these functions (for sure, you guessed it!) Solves only one problem. This is the principle of the only duty in action.

If I need to add, say, verification of form data, then instead of editing a huge function (and, possibly, breaking its work), I can simply add a new method to the project. This approach, in addition, allows grouping related variables and functions, which reduces the number of name conflicts, increases the reliability of the code and greatly simplifies the reuse of functions for other purposes if necessary.

So remember: one function - one task. Large functions should be processed into classes. If a function solves a multitude of tasks that are strongly related to each other, and in which the same data is used, it makes sense to redo it and turn it into an object with methods, much like I did with my large function for form processing.

Separation of commands and requests


Perhaps the funniest correspondence I've read is the one in which David Thorne communicates with Shannon Walkley about creating a poster to search for her missing cat, Missy. Every time Shannon asked David to make a poster, David did what she asked for, but added something from herself, in the end Shannon received something different from what she expected. This correspondence is very fun to read, but if your code does the same thing, it’s not funny anymore.

The separation of commands and requests forms the basis for protecting the code from unwanted side effects, which allows to avoid unpleasant surprises when calling functions. Functions are divided into two categories. One includes functions that execute commands, the second includes functions that execute queries. The first perform some actions, the second answer questions. Mix them is not recommended. Consider the following function:

 function getFirstName() {   var firstName = document.querySelector("#firstName").value;   firstName = firstName.toLowerCase();   setCookie("firstName", firstName);   if (firstName === null) {       return "";   }   return firstName; } var activeFirstName = getFirstName(); 

This is a simplified example, it is easy to see unexpected side effects that arise during the operation of the function. In reality, most side effects are much more difficult to find.

The name of the function, getFirstName , tells us that the function must return the name (first name), which it takes from somewhere (get). However, the first thing she does is convert the name taken from the document to lower case. The name of the function says that it should take something from somewhere (execute the query), but the function also changes the data state (that is, it executes some command). This is a side effect that is impossible to guess from the name of the function.

Further - worse. The function also writes the name requested from it to the cookie without telling us anything about it. This may well lead to the fact that it will overwrite something that we ourselves have written in the cookie, and what we expect. A function that performs a query in no case should not overwrite any data.

A useful rule that is worth adhering to is that if a function answers a question, it must return a value, and not change the state of the data. Conversely, if a function does something, it should change the data and should not return anything. For maximum clarity of the code, the same function should never return certain values ​​and modify the data.

Here is the improved version of the same code:

 function getFirstName() {   var firstName = document.querySelector("#firstName").value;   if (firstName === null) {       return "";   }   return firstName; } setCookie("firstName", getFirstName().toLowerCase()); 

This is a simple example, but, hopefully, it is clearly seen how the proposed separation of code into the one that performs the actions, and the one that returns the results, can clarify the programmer’s intentions and avoid errors. As the dimensions of functions and codebase grow, this separation becomes much more important, since searching for a definition of a function whenever it is going to be used, just to find out what exactly this function does, cannot be called effective use of someone else’s either time.

Loose binding


Let's think about the difference between puzzles and LEGO cubes. In the case of the puzzle, each piece of it can be combined with the others in only one way, and from all the pieces of the puzzle you can collect only one picture. If we talk about LEGO, then the cubes can be connected with each other as you like, collecting from the same blocks whatever you want. If you had to choose one of these types of building blocks to create something, before you knew exactly what you would create, what would you choose?

Linking is an indicator of how strongly one program block is dependent on others. Too much dependency (or strong binding) deprives the program of flexibility. That's how jigsaws are arranged and the like should be avoided. We strive for flexibility of the code, we want it to have the properties of LEGO cubes. And this is already called weak binding, and such organization of the code usually leads to much greater clarity.

Remember that the code must be flexible enough to be able to block with its help many options for its use. If you catch yourself copying and pasting code snippets and making small changes, or rewriting something because something has changed somewhere - you know - these are the consequences of strong binding. Another sign of a strong connectedness of program components is hard-coded IDs in functions, too many parameters of functions, many very similar functions, and the presence of large functions that violate the principle of a single duty. For example, in order to make the getFirstName function from the previous example suitable for reuse, the firstName in its code can be replaced with a certain universal ID that is passed to it as a parameter.

Strong binding is often manifested in groups of functions and variables, which, in fact, it would be good to arrange as a class. However, this can happen even when classes depend on methods or properties of other classes. If you encounter problems related to the interdependencies of functions, it may be time to think about creating a separate class from these functions.

I met with a similar one when I was working on the code for a set of interactive dials. Dials had a variety of variables, including those that determine their size, the size of the hand, the parameters of the axis of rotation, and so on. Because of this, the developer either had to use an awkward number of function parameters, or create several copies of each function with variables rigidly defined in them. In addition, the different dials behaved slightly differently. This led to the emergence of three sets of almost identical functions - one for each dial. If in a nutshell, the binding only intensified due to the hard-coded variables and due to the peculiarities of the behavior of objects, as a result, as in the case of the puzzle, there was only the only way to assemble it and make it work. The code was unnecessarily difficult.

We solved this problem by placing functions and variables in a class suitable for reuse, an instance of which was created for each of the three dials. We set up a class so that when creating its instance, it was possible to pass a function that influenced its behavior. As a result, it was possible to use the same class when creating copies of it for different dials. As a result, we had fewer functions, variables were stored in one place, which simplified the support of the project code.

Classes that interact with each other can also be the culprits of strong binding. Suppose there is a class that creates objects of another class, something like a college course represented by the CollegeCourse class, which can create students represented by the Student class. So the CollegeCourse class CollegeCourse working fine. But the time came when we needed to add a parameter to the constructor of the class Student . This is where the fun begins. In order to do this, we will need to modify the CollegeCourse class, making changes to it that correspond to changes in the Student class.

 var CollegeCourse = (function() {   function createStudent_WRONG(firstName, lastName, studentID) {       /*          Student ,     ,    !       */   }   function createStudent_RIGHT(optionsObject) {       /*               Student   .      ,         .       */   } }()); 

You should not need to modify the class due to changes in another class. This is a textbook example of strong binding. The constructor parameters can be transferred as an object, while it is possible to foresee the presence of default values, which weakens the links and means that the code will continue to work when new parameters are added.

As a result, we can say that it is recommended to build programs from blocks resembling LEGO cubes, rather than puzzle pieces. If you encounter problems resembling the above, it is very likely that they are caused by strong linking of code elements.

High connectivity


Have you ever seen a child clean up a room just dumping everything in the closet? Of course, outwardly, with the cabinet doors closed, it looks decent, but after such a “cleaning” nothing can be found, and things that have nothing in common with each other often end up in the same place. The same can happen with the code, if you do not strive for a high level of connectivity while writing it.

Connectivity is a measure of how different components of a program fit together. A high level of connectivity is good, it makes code snippets more clear. Low connectivity leads to confusion. Functions and methods in blocks of code must be connected in the sense of the actions they perform - that is, they must have a high level of connectivity.

A high level of connectivity means the collection of structures, united by a common idea, in one place. For example, a collection of functions for working with a database, or functions related to an aspect of a problem solved by an application, in one block of code or module. Such an approach not only helps to understand how these entities are organized, and where they can be found, but also helps prevent naming conflicts. If you have three dozen functions, the chances of naming conflicts are much higher than if there are 30 methods, logically divided into four classes.

If two or three functions use the same variables, then they must be somehow grouped. In fact, this is a good opportunity to combine them into an object. If you have a set of functions and variables that are used to control a page element, like a slider, from all of this you can make an object and increase the level of code connectivity within the application.

Remember the example about the dials, when thanks to the class you managed to avoid strong connectedness? This is a great example of how a high level of connectivity helps to combat strong entity bonding. In this case, a high level of connectivity and strong connectivity are at different ends of the "clarity scale", therefore, strengthening the connectivity, we weaken the connectivity, improving the code.

Duplicate code is a reliable sign of a low level of connectivity. Similar lines of code should be placed in functions, and similar functions should be reworked and form classes. Here it is useful to follow the rule that the same line of code should not be repeated twice. In practice, this is not always possible, but, striving for clarity of code, you should always think about how to reduce the number of repetitions of identical code fragments.

Similarly, the same data should not be stored in more than one variable. If you define variables with the same data in different places in the program, then you definitely need a class. Or, if you find that you are passing a link to the same HTML element in several functions, it is possible that this link should be made part of an instance of a certain class.

Objects can even be placed inside other objects in order to further increase the level of connectivity. For example, you can put AJAX functions in a single module that includes objects for sending a form, loading some information, and verifying user credentials used to log in to the system. Working with such constructions may look, for example, like this:

 Ajax.Form.submitForm(); Ajax.Content.getContent(7); Ajax.Login.validateUser(username, password); 

And on the contrary, it is not necessary to collect entities in one class that have no relation to each other. In the agency where I used to work, there was an internal API in which there was a Common object. In it was collected a whole bunch of various methods and variables that have nothing in common with each other. The class turned out to be just huge, it was inconvenient to work with it simply because when it was created no one thought about connectivity.

If properties are not used by several class methods, this may be a sign of a low level of connectivity. Similarly, if methods cannot be used in several different situations — or if the method is not used at all — this is also a sign of poor connectivity.

A high level of connectivity helps smooth the effects of strong binding, and strong code binding is a sign that a project needs a higher level of connectivity. As a rule, a high level of connectivity gives the developer more advantages than low connectivity, although both can usually be achieved together.

Results


If our code is far from ideal, problems arise. Achieving code clarity is much more than using the right indentation — it is a thorough planning done from the very beginning of the project. Although it is not easy to achieve the pinnacle of excellence in writing clear code, if you adhere to the principles of single duty, separation of commands and requests, weak connectivity and a high level of connectivity, you can significantly improve the clarity of your own code. This should be taken into account when working on any serious software project.

Dear readers! , , ? ?

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


All Articles