📜 ⬆️ ⬇️

Javascript style elements

In 1920, the book by William Stranka Jr. "Elements of Style" was published. Recommendations from it concerning the English language are still relevant today. The same principles applied to the code, can improve the quality of programs.

image

It should be noted that we are not talking about hard and fast rules. What we will talk about today is just recommendations. Even if you decide to follow them, there may well be good reasons for deviating from them, for example, if this helps to make the code clearer. However, in doing so, be careful and remember that people are subject to cognitive distortion . For example, choosing between ordinary and arrow functions in JavaScript, those who are not very familiar with the latter, prefer ordinary functions, by virtue of the habit of considering them to be clearer, simpler, more convenient.

The principles of the “Elements of Style” are not by chance alive today. The fact is that usually their use makes the texts better. Usually the author of the book is right. It is worth deviating from them only in those cases when there is a good reason for this - and not because of whims or personal preferences.
')
Many of the recommendations from the chapter "Basic principles of composition" apply to the program code:

  1. Make the paragraph the minimum part of the composition. One paragraph - one topic.
  2. Avoid unnecessary words.
  3. Use a valid deposit.
  4. Avoid sequences of weakly related sentences.
  5. Words in sentences related to each other should not be separated by other language constructs.
  6. Use affirmative statements.
  7. Express similar in meaning and purpose of thought in a similar form, using parallel structures.

Almost the same can be said about the style of the code:

  1. Make the minimum part of the composition function. One function - one task.
  2. Avoid unnecessary code.
  3. Use a valid deposit.
  4. Avoid sequences of loosely connected language constructs.
  5. Keep in one place the code and other elements of the programs aimed at solving one problem.
  6. Use an affirmative form for variable names and when building expressions.
  7. Use the same templates for solving similar problems.

Function as a unit of composition


The essence of software development is composition. We create programs by composing modules, functions, and data structures.

Understanding the process of creating functions and how to use them with other functions is one of the fundamental skills of a programmer.

A module is a collection of functions or data structures. Data structures are how we represent the state of a program. However, nothing interesting happens until it comes to using functions.

There are three types of functions in JavaScript.


Functions for implementing I / O operations and some data processing algorithms are needed almost everywhere, but the vast majority of the functions that you have to use will be mapping.

▍One task - one function


If your function is intended to perform I / O operations, do not engage in mapping tasks. If the function is intended for mapping, do not perform I / O operations in it.

It must be said that procedural functions violate both the rule “one function - one task” and the rule concerning weakly related language constructs. However, one cannot do without such functions.

The ideal function is a simple, deterministic, pure function with the following basic properties:


Redundant code


The energetic text is concise. The sentence should not contain unnecessary words, in the paragraph - unnecessary sentences, for the same reason that there should not be unnecessary lines in the drawing, and unnecessary details in the mechanism. This does not mean that the writer should use only short sentences, or, avoiding details, get along with general descriptions. This means that every word should have a meaning. [Unnecessary words omitted].

William Strank Jr., "Elements of Style"

Laconic code is very important in software development, since the more code - the more places to make a mistake. A smaller amount of code means fewer places where an error can disappear, which leads to a decrease in the number of errors.

Laconic code is easier to read, as it has a higher level of payload to information "interference". The reader needs to weed out less syntactic "noise" in order to understand the meaning of the program. Thus, a smaller amount of code means less syntactic "noise", and, as a result, a clearer transfer of meaning.

To put it in the words of the “Style Elements”, the compressed code is energetic code. For example, such a construction:

function secret (message) {  return function () {    return message;  } }; 

It can be reduced to this:

 const secret = msg => () => msg; 

Those who are familiar with the minimalist syntax characteristic of switch functions (they appeared in ES6 in 2015) are easier to read this entry, and not the code from the first example. Unnecessary elements, such as parentheses, the function keyword, the return , are omitted here.

In the first version, there are many service syntax constructs. These include parentheses, the function keyword, and return . They, for someone who is familiar with the switch functions, are nothing more than syntactic “noise”. And, in modern JavaScript, such constructions exist only so that the code can be read by those who are not yet confident enough to own ES6. Although, ES6 became the standard of the language in 2015, so the time to get to know it better has long come.

Unnecessary variables


Sometimes we give a name to something, for which it is not very necessary. Say, some intermediate variable, without which you can do. Why is it harmful? The problem here is that the human brain has limited short-term memory resources . Having encountered a variable in the program's text, we are forced to memorize it. If there are a lot of names, our memory is full, while reading we occasionally have to go back ...

That is why experienced developers train themselves to eliminate unnecessary variables.

For example, in most situations, variables created only to store the return value of a function should be avoided. The name of the function should provide adequate information about what the function will return. Consider an example:

 const getFullName = ({firstName, lastName}) => { const fullName = firstName + ' ' + lastName; return fullName; }; 

Getting rid of unnecessary code can be rewritten as follows:

 const getFullName = ({firstName, lastName}) => ( firstName + ' ' + lastName ); 

Another common approach to reducing the number of variables is to use the composition of functions and the so-called “pointless notation”.

Pointless notation is a way to declare functions without mentioning the arguments with which these functions operate. The usual ways of applying this approach are currying and composition of functions.

Here is an example of currying:

 const add2 = a => b => a + b; //       inc(), //    1   . const inc = add2(1); inc(3); // 4 

Take a look at the inc() function declaration. Notice that neither the function keyword nor the syntax elements specific to the declaration of pointer functions are present. There is also no description of the parameters of the function, since the function does not use them. Instead, it returns another function that knows what to do with the arguments passed to it.

Take a look at an example that uses composition of functions. The composition of functions is the application of a function to the results returned by another function. Whether you realize it or not, you apply composition of functions all the time.

For example, when you use chains of method calls like .map() and promise.then() . If we turn to the most general form of writing the composition of functions, we get the following construction: f(g(x)) . In mathematics, this is usually written as f ∘ g , which is read as “applying the function f to the result of the function g ”.

Using the composition of two functions, you eliminate the need to create a variable to store the intermediate value between function calls.

Let's see how this technique allows you to write more clean code:

 const g = n => n + 1; const f = n => n * 2; //    : const incThenDoublePoints = n => { const incremented = g(n); return f(incremented); }; incThenDoublePoints(20); // 42 // compose2 —        const compose2 = (f, g) => x => f(g(x)); //   : const incThenDoublePointFree = compose2(f, g); incThenDoublePointFree(20); // 42 

The same can be done with any function.

A functor is an object that implements the mapping function. For example, in JS these are arrays ( Array.map() ) or promises ( promise.then() ). We write another version of the compose2 function, using a chain of calls to the mapping functions for the purpose of function composition:

 const compose2 = (f, g) => x => [x].map(g).map(f).pop(); const incThenDoublePointFree = compose2(f, g); incThenDoublePointFree(20); // 42 

You do almost the same thing every time, using call chains in promises.

In fact, each functional programming library implements at least two ways of composition of functions. This is the compose() function, which applies functions from right to left, and pipe() , which applies functions from left to right.

For example, in Lodash such functions are called, respectively, compose() and flow() . When I use this library, I use the flow() function like this:

 import pipe from 'lodash/fp/flow'; pipe(g, f)(20); // 42 

However, this functionality can be implemented independently, without libraries:

 const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x); pipe(g, f)(20); // 42 

If the aforementioned seems to you to be something very abstruse, and you do not know how you would use all this, consider the following:

The essence of software development is composition. We create programs by composing small modules, functions, and data structures.

Understanding tools for the composition of functions and objects is just as important for a programmer as for a builder — the ability to control a drill and a mounting gun. And the use of an imperative code for combining functions and the unjustified use of variables for storing intermediate results resembles assembling furniture with adhesive tape.

As a result, we offer you to remember the following:


Active voice


Actual voice usually means a clearer and more lively expression of thought than a passive.

William Strank Jr., "Elements of Style"

Give software constructions as clear and precise names as possible:


Call the predicate functions and logical variables as if they were questions that allowed the answer “yes” or “no”:


Use verb forms in function names:


Event Handlers


The naming of event handlers and life-cycle methods is an exception to the rule for using verbs in function names, as they are used as qualifiers. They show not “what” to do, but “when.” They should be named according to the following scheme: "<when ​​to perform an action>, <verb>".


The names of event handlers from the list that are recognized as unsuccessful look as if we want to trigger an event, and not respond to it.

Life Cycle Techniques


Take a look at the following variants of the hypothetical component life cycle methods that are created to call the handler function before updating this component:


In the first example, we use the passive voice (“will be updated”, not “update”). This name is redundant, it is not clearer than other options.

The second example looks better, but the point of this life cycle method is to call the handler. The componentWillUpdate(handler) name is read as if the component is going to affect the handler, update it, which does not express the true value of this software construct. We mean the following: "Before the component is updated, call the handler." The name beforeComponentUpdate() expresses our intention most clearly.

We can continue on the path of simplification. Since we are talking about the methods of the object, when they are called, the object itself will be mentioned. This means that adding an object name to a method name is redundant. Think about how the following construct will look like if you call a method when accessing a component: component.componentWillUpdate() . It will be read in the same way as: “Vasya Vasya will have cutlets for lunch”. Double mentioning an object name is redundant. As a result, the following is obtained: component.beforeUpdate(doSomething) better than c omponent.beforeComponentUpdate(doSomething) .

Functional impurities are functions that add properties and methods to objects. Such functions are called one after another in a conveyor that resembles an assembly line in a factory. Each function accepts an instance as an input, an object, and adds something to it before passing the next function in the pipeline.

I prefer to call such functions using adjectives. In order to find the right word, you can use the suffixes "ing" and "able". Here are some examples:


Sequences of weakly related language constructs


... a series of statements soon becomes monotonous and boring.

William Strank, Jr., "Elements of Style."

Developers fill functions with sequences of language constructs. These constructions are conceived so that they are executed one after another, in essence, being an example of a series of loosely related statements. Such an approach, when too many such calls are collected in a certain program block, leads to the appearance of the so-called “spaghetti code”.

In addition, call sets are often repeated in a variety of similar forms. In this case, each of the repeating blocks may well differ slightly from others, and often such differences arise quite unexpectedly. For example, the basic needs of a certain user interface component correspond to the needs of almost all such components. Implementing what is needed by all these components can be based on the different stages of their life cycle, breaking the implementation into several functions.

Consider the following call sequence:

 const drawUserProfile = ({ userId }) => { const userData = loadUserData(userId); const dataToDisplay = calculateDisplayData(userData); renderProfileData(dataToDisplay); }; 

This function performs three different cases: loading data, building, based on what was loaded, the data model of the interface element, and outputting the element to the page.

In most modern libraries for the development of interfaces, each of the above tasks is solved separately from the others, for example, using a dedicated function. Separating these tasks, we can combine functions without special problems, achieving the desired result in various situations.

With this approach, we could completely replace, say, the output function of the component, and it would not affect other parts of the program. In React, for example, there are many rendering subsystems designed for different platforms and different library usage scenarios. Here is a far from complete list: ReactNative for native iOS and Android applications, AFrame for WebVR, ReactDOM / Server for rendering components on the server side.

Another problem with the above function is that it does not allow preparing the interface element model and displaying it on the page without first loading the initial data. What if this data is already uploaded? Ultimately, if a similar function that combines several operations in itself is called several times, this leads to the execution of unnecessary actions.

Separation of operations, in addition, opens the way to their independent testing. In the process of writing code, I constantly run unit tests in order to immediately assess the impact on the application of the changes made to it. However, if, as in our example, we combine the rendering code of the control with the source data loading code, it will not be possible to simply transfer some conditional data to the output function of the element for test purposes. There will have to test everything - and download, and preparation, and data output. This, if you need to check only one thing, will lead to unnecessary time expenses: data, for example, must be downloaded over the network, processed, displayed in the browser ... To get the test results you will have to wait longer than when checking a separate component. Separation of functions will allow testing them separately from other parts of the application.

In our example, there are already three separate functions, the calls of which can be easily put into different methods of the component life cycle. For example, you can load source data when a component is connected, processing of this data and displaying the component on the screen can be performed in response to an event related to updating the state of an interface element.

The application of the above principles leads to the emergence of software with more clearly defined areas of responsibility of its individual components. Each of the components can reuse the same data structures and life cycle event handlers; as a result, we don’t perform actions that can be performed only once.

Storing code and other elements of programs aimed at solving one problem


Many frameworks and templates provide for organizing program files by their type. If we are talking about a simple project, such as a small calculator, or a ToDo application, this approach will not cause problems, but in larger projects it is better to group the files according to the functionality of the application, which they implement.

For example, here are two variants of the file hierarchy for a ToDo application. The first option is to group files by type:

 . ├── components │   ├── todos │   └── user ├── reducers │   ├── todos │   └── user └── tests   ├── todos   └── user 

The second - grouping according to the logical principle:

 . ├── todos │   ├── component │   ├── reducer │   └── test └── user   ├── component   ├── reducer   └── test 

Grouping files according to the functionality they implement allows, if you need to make changes to some part of the application, not to constantly move from folder to folder in search of the necessary files.

As a result, we recommend grouping files based on what functionality of the application they implement.

Using an affirmative form for variable names and when building expressions


Make clear statements. Avoid sluggish, colorless, indecisive, evasive language. Do not use the word as a means of denial, in antithesis, or as a way to avoid the topic.

William Strank Jr., "Elements of Style"

Let's go straight to examples of variable names:


▍Conditional Operator


This design:

 if (err) return reject(err); //  -... 

... better this:

 if (!err) { // ...  - } else { return reject(err); } 

Ternal Operator


So:

 { [Symbol.iterator]: iterator ? iterator : defaultIterator } 

... better than this:

 { [Symbol.iterator]: (!iterator) ? defaultIterator : iterator } 

▍About Negative Statements


Sometimes a logical variable interests us only in situations where its value is false. Using a name for such a variable in an affirmative form will result in the need to use the logical negation operator, when checking it,! . In such cases, it is better to give the variables distinct negative names. The word "not" in the variable name and the operator ! in comparison operations lead to vague wording. Consider a few examples.

if (missingValue) better than if (!hasValue)
if (anonymous)
if (!hasValue)
if (anonymous)
if (!hasValue)
if (anonymous)
better than if (!user)
if (isEmpty(thing))
if (!user)
if (isEmpty(thing))
if (!user)
if (isEmpty(thing))
better than if (notDefined(thing)) .

▍Arguments of functions that take null and undefined values


Do not create functions that, when called, require undefined or null instead of optional parameters. In such situations, it is best to use an object with named parameters:

 const createEvent = ({ title = 'Untitled', timeStamp = Date.now(), description = '' }) => ({ title, description, timeStamp }); // ... const birthdayParty = createEvent({ title: 'Birthday Party', description: 'Best party ever!' }); 

…better than:

 const createEvent = ( title = 'Untitled', timeStamp = Date.now(), description = '' ) => ({ title, description, timeStamp }); // ... const birthdayParty = createEvent( 'Birthday Party', undefined, //     'Best party ever!' ); 

Patterns and solution of similar tasks


... parallel construction requires external similarity of fragments of text that have similar content and purpose. The similarity of the form allows the reader to more easily recognize the similarity of the content.

William Strank Jr., "Elements of Style"

When creating applications, the programmer often has to solve very similar tasks. A code that can be repeated is usually much more than a completely unique one. As a result, during the work you have to constantly do the same thing. The good thing here is that it provides an opportunity for generalizing similar code and creating abstractions. To do this, it is enough to identify the same parts of the code, select them and use them wherever they are needed. And in the course of development, to pay attention only to unique constructions for a particular fragment of the application. In fact, various libraries and frameworks serve this purpose.

Here, for example, are the components of the user interface. And ten years have not passed since it was commonplace to lump the interface updates using jQuery, the application logic and the organization of its interaction with the outside world. Later, programmers began to realize that in client web applications, MVC could be used, and they began to separate models from the interface update logic.

As a result, web applications began to be built using a component approach, which allowed for declarative modeling of components using something like templates created using HTML or JSX.

All this has led to the use of the same user interface update logic for all components, which is much better than the unique imperative code.

Those familiar with the components, it is very easy to understand how they work, even if we are talking about an application unfamiliar to them. , , , , , , , , .

, , , , .

: ,


ES6 2015-, , , . , , , , , . — , , . , — . , , ES6 , ES5. .

, , . , :


, , , :


, :


, , , , . . , , , , , , .

, , .

, . , . , ES6, , , , , « » . , , , -, JS, .

Dear readers! ES6 ?

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


All Articles