The software is constantly becoming more complex. The stability and ease of extending the application is directly dependent on the quality of the code.
Unfortunately, almost every developer, including myself, in his work faces a code of poor quality. And this is a swamp. This code has toxic signs:
Everyone is familiar with the statements “I don’t understand how this code works,” “crazy code”, “this code is difficult to change” and others.
One day, my colleague quit because he was trying to cope with a Ruby REST API that was difficult to maintain. He received this project from a previous development team.
Correction of current errors created new ones, adding new functions gave rise to a new series of errors, and so on (fragile code). The client did not want to rebuild the application, make it a convenient structure, and the developer made the right decision - to quit.
Such situations happen often, and this is sad. But what to do?
First, remember: create a working application and take care of the quality of the code - different tasks.
On the one hand, you implement the requirements of the application. But on the other hand, you have to spend time and check if too many tasks hang on any function, give meaningful names to variables and functions, avoid functions with side effects and so on.
Functions (including object methods) are small gears that make an application work. At the beginning you should focus on their structure and composition. The article covers the best approaches, how to write simple, understandable and easily testable functions.
Avoid bloated functions that have a lot of tasks, it is better to do a few small functions. Bloated functions with hidden meaning are difficult to understand, modify, and, especially, test.
Imagine a situation where a function must return the sum of the elements of an array, a map, or a simple JavaScript object. The amount is calculated by folding the property values:
null
or undefined
For example, the sum of the array [null, 'Hello World', {}]
calculated as follows: 1
(for null
) + 2
(for the string, primitive type) + 4
(for the object) = 7
.
Let's start with the worst method. The idea is to write code with one big function getCollectionWeight()
:
function getCollectionWeight(collection) { let collectionValues; if (collection instanceof Array) { collectionValues = collection; } else if (collection instanceof Map) { collectionValues = [...collection.values()]; } else { collectionValues = Object.keys(collection).map(function (key) { return collection[key]; }); } return collectionValues.reduce(function(sum, item) { if (item == null) { return sum + 1; } if (typeof item === 'object' || typeof item === 'function') { return sum + 4; } return sum + 2; }, 0); } let myArray = [null, { }, 15]; let myMap = new Map([ ['functionKey', function() {}] ]); let myObject = { 'stringKey': 'Hello world' }; getCollectionWeight(myArray); // => 7 (1 + 4 + 2) getCollectionWeight(myMap); // => 4 getCollectionWeight(myObject); // => 2
The problem is clearly visible. The getCollectionWeight()
function is too bloated and looks like a black box full of surprises.
Most likely, at first glance it is difficult for you to understand what her task is. And imagine a set of such functions in the application.
When you work with such a code, you waste time and effort. A quality code will not cause you discomfort. High-quality code with short and silent functions is nice to read and easy to maintain.
Now the goal is to break the long function into small, independent and reusable ones. The first step is to extract the code that determines the sum of the value by its type. This new function will be called getWeight()
.
Also note the magic numbers of this amount: 1
, 2
and 4
. Just reading these numbers, without understanding the whole story, does not provide useful information. Fortunately, ES2015 allows you to declare const
as read-only, so you can easily create constants with meaningful names and eliminate magic numbers.
Let's create a small getWeightByType()
function and at the same time improve getCollectionWeight()
:
// Code extracted into getWeightByType() function getWeightByType(value) { const WEIGHT_NULL_UNDEFINED = 1; const WEIGHT_PRIMITIVE = 2; const WEIGHT_OBJECT_FUNCTION = 4; if (value == null) { return WEIGHT_NULL_UNDEFINED; } if (typeof value === 'object' || typeof value === 'function') { return WEIGHT_OBJECT_FUNCTION; } return WEIGHT_PRIMITIVE; } function getCollectionWeight(collection) { let collectionValues; if (collection instanceof Array) { collectionValues = collection; } else if (collection instanceof Map) { collectionValues = [...collection.values()]; } else { collectionValues = Object.keys(collection).map(function (key) { return collection[key]; }); } return collectionValues.reduce(function(sum, item) { return sum + getWeightByType(item); }, 0); } let myArray = [null, { }, 15]; let myMap = new Map([ ['functionKey', function() {}] ]); let myObject = { 'stringKey': 'Hello world' }; getCollectionWeight(myArray); // => 7 (1 + 4 + 2) getCollectionWeight(myMap); // => 4 getCollectionWeight(myObject); // => 2
Does it look better?
The getWeightByType()
function is an independent component that simply determines the sum by type. And it is reusable because it can be executed within any other function.
getCollectionWeight()
becomes a bit more lightweight
WEIGHT_NULL_UNDEFINED
, WEIGHT_PRIMITIVE
and WEIGHT_OBJECT_FUNCTION
are non-explanatory constants that describe types of sums. You do not need to guess what the numbers 1
, 2
and 4
mean.
The updated version still has flaws.
Imagine that you have a plan to implement a comparison of the values ​​of a Set or another arbitrary collection. getCollectionWeight()
will quickly grow in size, as its logic is to collect values.
Let's extract the code that collects the values ​​from the getMapValues​​()
map and simple getMapValues​​()
JavaScript objects into separate functions. Look at the improved version:
function getWeightByType(value) { const WEIGHT_NULL_UNDEFINED = 1; const WEIGHT_PRIMITIVE = 2; const WEIGHT_OBJECT_FUNCTION = 4; if (value == null) { return WEIGHT_NULL_UNDEFINED; } if (typeof value === 'object' || typeof value === 'function') { return WEIGHT_OBJECT_FUNCTION; } return WEIGHT_PRIMITIVE; } // Code extracted into getMapValues() function getMapValues(map) { return [...map.values()]; } // Code extracted into getPlainObjectValues() function getPlainObjectValues(object) { return Object.keys(object).map(function (key) { return object[key]; }); } function getCollectionWeight(collection) { let collectionValues; if (collection instanceof Array) { collectionValues = collection; } else if (collection instanceof Map) { collectionValues = getMapValues(collection); } else { collectionValues = getPlainObjectValues(collection); } return collectionValues.reduce(function(sum, item) { return sum + getWeightByType(item); }, 0); } let myArray = [null, { }, 15]; let myMap = new Map([ ['functionKey', function() {}] ]); let myObject = { 'stringKey': 'Hello world' }; getCollectionWeight(myArray); // => 7 (1 + 4 + 2) getCollectionWeight(myMap); // => 4 getCollectionWeight(myObject); // => 2
getCollectionWeight()
reading getCollectionWeight()
it's much easier for you to understand what this function does. Looks like an interesting story.
Each function is obvious and intelligible. You do not waste time trying to understand what such code does. That's how clean it should be.
Even at this stage you have many opportunities for quality improvement!
You can create a separate getCollectionValues​​()
that contains if / else statements and differentiates the types of collections:
function getCollectionValues(collection) { if (collection instanceof Array) { return collection; } if (collection instanceof Map) { return getMapValues(collection); } return getPlainObjectValues(collection); }
Then getCollectionWeight()
becomes really simple, because the only thing to do is to get the values ​​of the getCollectionValues​()
collection and apply a sum reducer to it.
You can also create a separate shortcut function:
function reduceWeightSum(sum, item) { return sum + getWeightByType(item); }
Because ideally, getCollectionWeight()
should not define functions.
In the end, the initial big function turns into small:
function getWeightByType(value) { const WEIGHT_NULL_UNDEFINED = 1; const WEIGHT_PRIMITIVE = 2; const WEIGHT_OBJECT_FUNCTION = 4; if (value == null) { return WEIGHT_NULL_UNDEFINED; } if (typeof value === 'object' || typeof value === 'function') { return WEIGHT_OBJECT_FUNCTION; } return WEIGHT_PRIMITIVE; } function getMapValues(map) { return [...map.values()]; } function getPlainObjectValues(object) { return Object.keys(object).map(function (key) { return object[key]; }); } function getCollectionValues(collection) { if (collection instanceof Array) { return collection; } if (collection instanceof Map) { return getMapValues(collection); } return getPlainObjectValues(collection); } function reduceWeightSum(sum, item) { return sum + getWeightByType(item); } function getCollectionWeight(collection) { return getCollectionValues(collection).reduce(reduceWeightSum, 0); } let myArray = [null, { }, 15]; let myMap = new Map([ ['functionKey', function() {}] ]); let myObject = { 'stringKey': 'Hello world' }; getCollectionWeight(myArray); // => 7 (1 + 4 + 2) getCollectionWeight(myMap); // => 4 getCollectionWeight(myObject); // => 2
This is the art of creating small and simple functions!
After all the optimization of the quality of the code, a handful of good benefits appear:
getCollectionWeight()
simplified thanks to the code that does not require explanationgetCollectionWeight()
size has decreased significantlygetCollectionWeight()
function is now protected from rapid growth if you want to implement work with other types of collections.These benefits will help you survive in the complex structure of applications.
The general rule is that functions should not exceed 20 lines of code. Less is better.
I think you now have a fair question: “I don’t want to create a function for each line of code. Are there any criteria when to stop? ” This is the topic of the next chapter.
Let's get a little distracted and think about what an application is?
Each application implements a set of requirements. The task of the developer is to divide these requirements into small executable components (scopes, classes, functions, blocks of code) that perform well-defined operations.
The component consists of other smaller components. If you want to write code for a component, you need to create it from components of only the previous level of abstraction.
In other words, it is necessary to decompose the function into smaller steps, but all of them should be on the same, previous, level of abstraction. This is important because the function becomes simple and implies "the accomplishment of one task, and the execution is qualitative."
What is the need? Simple functions are obvious. Evidence means easy reading and modification.
Let's try to follow the example. Suppose you want to implement a function that stores only the prime numbers (2, 3, 5, 7, 11, etc.) of the array and removes the rest (1, 4, 6, 8, etc.). The function is called like this:
getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
What steps of the previous level of abstraction are needed to implement the getOnlyPrime()
function? Let's formulate this:
To implementgetOnlyPrime()
filter the array of numbers using theIsPrime()
function.
Simply apply the IsPrime()
filter IsPrime()
to the array.
Is it necessary to implement IsPrime()
details at this level? No, because then the getOnlyPrime()
function will have steps from a different level of abstractions. The function will take on too many tasks.
Without forgetting this simple idea, let's implement the body of the getOnlyPrime()
function:
function getOnlyPrime(numbers) { return numbers.filter(isPrime); } getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
As you can see, getOnlyPrime()
is an elementary function. It contains steps from one level of abstraction: the .filter()
method and IsPrime()
.
Now it's time to move to the previous level of abstraction.
The array .filter()
method is included in JavaScript and is used as is. Of course, the standard describes exactly what the method does.
Now you can specify how IsPrime()
will be implemented:
To implement theIsPrime()
function, which checks whether the number n is simple, you need to check whether n is divisible by any number from2
toMath.sqrt(n)
without a remainder.
Let's write the code for the IsPrime()
function using this algorithm (it's not efficient yet, I used it for simplicity):
function isPrime(number) { if (number === 3 || number === 2) { return true; } if (number === 1) { return false; } for (let divisor = 2; divisor <= Math.sqrt(number); divisor++) { if (number % divisor === 0) { return false; } } return true; } function getOnlyPrime(numbers) { return numbers.filter(isPrime); } getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
getOnlyPrime()
is small and elementary. It only strictly necessary steps of the previous level of abstraction.
Reading complex functions can be greatly simplified by following the rule to make them obvious. If the code of each level of abstraction is written pedantically, it will prevent the generation of large pieces of uncomfortable code.
Function names must be compact: no more and no less. Ideally, the name should clearly indicate what the function does, without having to rummage through the implementation details.
For function names, use the camel case format, which begins with a small letter: addItem()
, saveToStore()
or getFirstName()
.
Since a function is an action, its name must contain at least one verb. For example, deletePage()
, verifyCredentials()
. To get or set a property, use the prefixes set and get: getLastName()
or setLastName()
.
In production, avoid confusing names like Foo()
, bar()
, ()
, fun()
and the like. Such names do not make sense.
If the functions are small and simple, and the names are compact: the code reads like a good book.
Of course, the examples given are straightforward. Applications that exist in reality are more complex. You can complain that writing simple functions of the previous level of abstraction is a tedious task. But it is not so laborious if you do it from the very beginning of the project.
If the application already has functions that are too bloated, it will most likely be difficult to rebuild the code. And in many cases it is impossible in reasonable time intervals. Begin, at least with small: extract what you can.
Of course, the right decision is to implement the application correctly from the very beginning. And invest time not only in the implementation, but also in the correct structure of functions: to make them small and simple.
Seven times measure cut once.
In ES2015, a good modular system is implemented, which clearly shows that small functions are good practice.
Just remember that clean and organized code always takes an investment of time . You may be difficult. You may need to practice for a long time. You can go back and change functions several times.
There is nothing worse than a dirty code.
What methods do you use to make the code organized?
(Translation by Natalia Bass )
Source: https://habr.com/ru/post/310590/
All Articles