Functions, Methods and Tricks [PART 2] By albro

avatar

Functions, Methods and Tricks

Let's continue the discussion from the previous post and talk more about writing clean functions. As I said, methods are structurally similar to functions, so there is no difference between them.

Unlimited number of parameters

As you know, we can have an unlimited number of parameters in many programming languages such as JavaScript and Python. In JavaScript, we do this by writing three dots next to the parameter name. Consider the following example:

function sumUp(...numbers) {
  let sum = 0;
  for (const number of numbers) {
    sum += number;
  }
  return sum;
}


const total = sumUp(10, 19, -3, 22, 5, 100);

Such a function accepts any number of arguments, so we can pass any number to it, and its work is also clear: adding up all the passed numbers! With this account, it can be said that such functions are exceptions and even though the number of arguments passed to them is very large, they are still readable and have no problems. Naturally, rewriting these types of functions is possible; For example, we can write it in such a way that instead of accepting different numbers, it only accepts an array of different numbers:

const total = sumUp([10, 19, -3, 22, 5, 100]);

But I personally don't have a preference for it and I think that the first mode and normal passing of numbers is suitable because even the order of passed numbers is not important.

Output parameters

Output parameters are parameters that are not taken for input and data processing but are returned as an output value. what does it mean? Suppose we have a function called createId that is responsible for creating an ID for a user:

createId(user)

user here must be the input parameter, that is, our function takes the user object and generates an ID for it and then returns that ID to us, but sometimes we may have a function where User is the output parameter! what does it mean? That is, the function takes the user object and adds an id field to it, and finally returns the whole new user object. Example:

function createId(user) {
  user.id = "u1";
}


const user = { name: "albro" }; createId(user);


console.log(user);

As you can see, this function adds a new field called id to the user object. Doing so means using output parameters, and output parameters should be avoided as much as possible due to confusion for other developers. Although sometimes, due to working with some frameworks and libraries, we cannot exclude them 100%. If you can't avoid output parameters, you should at least know them and choose your function names to reflect this. For example, use addId instead of createId.

A better way (if you must use output parameters) is to write the same code in object-oriented form:

class User {
  constructor(name) {
    this.name = name;
  }


addId() { this.id = "u1"; } }


const customer = new User("albro"); customer.addId(); console.log(customer);

In this method, the readability of the code increases because at least we know that we still have the customer object, and by calling addId, an id has probably been added to this object. In this way, editing of the main object is quite obvious and not as bad as the previous case.

The body of the function

As I said, when it comes to writing clean code in functions, we can divide each function into two parts: function definition and function invocation. One of the most important rules related to clean functions is their length or volume. Consider the following function:

function renderContent(renderInformation) {
  const element = renderInformation.element;
  if (element === 'script' || element === 'SCRIPT') {
    throw new Error('Invalid element.');
  }


let partialOpeningTag = '<' + element;


const attributes = renderInformation.attributes;


for (const attribute of attributes) { partialOpeningTag = partialOpeningTag + ' ' + attribute.name + '="' + attribute.value + '"'; }


const openingTag = partialOpeningTag + '>';


const closingTag = ''; const content = renderInformation.content;


const template = openingTag + content + closingTag;


const rootElement = renderInformation.root;


rootElement.innerHTML = template; }

This function is very long and does a lot of things. Without reading the rest of this post, I want you to look at the code above and try to understand how this function works. Do not rush and give yourself a few minutes.

If you've spent time, you'll find that this function is annoying to understand because it's so long. The task is to render HTML elements in HTML pages. If I want to briefly explain how it works, it will be that we receive an object called renderInformation as a parameter and separate the element property from it. This property will have the name of our HTML element. First, we check that the desired element is not a script, because the script element causes JavaScript codes to run on our pages, and it is very dangerous to run them automatically. In the next step, we extract HTML attributes from the same renderInformation object and paste them to our element. In the next step, we put the content inside the tag (content in the code above) inside the tag and finally enter it into HTML.

If we want to look at this function fairly, it should be said that the naming rules are well followed and the formatting or spaces are done correctly, but this function is still not clean enough. If another developer takes a look at this function, it will still be a mess. Why? Because the amount of codes inside the function is large, reading them and understanding them will take a relatively long time. If we want to write this function cleanly, we must break it into other functions:

function renderContent(renderInformation) {
  const element = renderInformation.element;
  const rootElement = renderInformation.root;


validateElementType(element);


const content = createRenderableContent(renderInformation);


renderOnRoot(rootElement, content); }

If we give this function to a developer instead of the previous function, it will be much easier for him/her to understand how the function works. In this function, we have taken the renderInformation object and then separated the desired element from it. In the next step, we have also obtained the root or main element of our HTML page. In the next step, we have a function called validateElementType, which means "element type validation", so its work is clear. The next functions are createRenderableContent (creating content that can be displayed) and renderOnRoot (displaying on the root element), which you can understand by reading their names. Understanding this function is much easier than the previous function.

Note that we have not changed the mechanism of action for this function, but we have edited different parts of the code in the form of a separate function. By doing this, the overall size of our code has not changed much, but the main function has become much more readable. Also keep in mind that like it or not, large projects have a lot of code, so when you open a file, you will see hundreds of lines of code. When we talk about the length of the codes, we mean the length of the codes at the level of a specific function. For example, if I put the above function together with all its accompanying functions, we will have such codes:

function renderContent(renderInformation) {
  const element = renderInformation.element;
  const rootElement = renderInformation.root;


validateElementType(element);


const content = createRenderableContent(renderInformation);


renderOnRoot(rootElement, content); }


function validateElementType(element) { if (element === 'script' || element === 'SCRIPT') { throw new Error('Invalid element.'); } }


function createRenderableContent(renderInformation) { const tags = createTags( renderInformation.element, renderInformation.attributes ); const template = tags.opening + renderInformation.content + tags.closing; return template; }


function renderOnRoot(root, template) { root.innerHTML = template; }


function createTags(element, attributes) { const attributeList = generateAttributesList(attributes); const openingTag = buildTag({ element: element, attributes: attributeList, isOpening: true, }); const closingTag = buildTag({ element: element, isOpening: false, });


return { opening: openingTag, closing: closingTag }; }


function generateAttributesList(attributes) { let attributeList = ''; for (const attribute of attributes) { attributeList = `${attributeList} ${attribute.name}="${attribute.value}"`; }


return attributeList; }


function buildTag(tagInformation) { const element = tagInformation.element; const attributes = tagInformation.attributes; const isOpeningTag = tagInformation.isOpening;


let tag; if (isOpeningTag) { tag = '<' + element + attributes + '>'; } else { tag = ''; }


return tag; }

Although the volume of these codes is large in general, each of the functions does not exceed its own limits. The main issue here is that our hypothetical developer wants to know what the renderContent function does. If we write the codes in this way, the developer can understand how renderContent works at a glance, and there is usually no need to check other functions, although this is also possible, and each of the subordinate functions is also simple and understandable. Most of the time developers work like this and don't care about the details of many functions. Breaking large functions into smaller functions helps with this.

Levels of operation

The general rule about functions is that each function must do a specific job. You are probably asking how do we define "work"? By "work" we mean high-level operations. what does it mean? For example, validating an email and saving a user in a database are two high-level operations in abstraction, but checking for the presence of the @ sign in an email is a low-level operation, or simply too partial. In high-level operations, we don't know how to do the operation and we don't look for details, we only know the result. For example, in saving the user in the database, we only know that the database receives and stores the user, but from the low level, the user is first passed to the database driver, and this driver communicates with the database server and so on.

You might be asking yourself, does this mean we should always write high-level functions? Definitely not. If we want to speak in more detail, we will consider a general rule: functions should do something that is one level below their name! what does it mean? Consider the following example:

function emailIsValid(email) {
    return email.includes('@');
}

In the example above, we have a function called emailIsValid, which means "email is valid". Naturally, this is a simple code that is only used for example, and it is never easy to validate emails in this way. In this code, we check the presence of the @ sign in the email, which is a low-level code. This low-level operation is one level below the "email is valid" level. Although this code is low level, it makes sense because of the name we gave to the function. The includes function, which is one of the built-in JavaScript functions, has no special meaning and checks the presence of one or more special characters in a string. It is us as developers who give this function a special meaning. Therefore, low-level code is not bad and it is not possible to run a program without writing low-level code, but you must remember the rule I gave you in the example above so that your code is clean.

At the same time, if we had a function named saveUser that performs the validation operation and saving the user in the database at a low level, we would have a very unreadable code and we would not follow the mentioned rule. saveUser contains all the steps that lead to saving the user (retrieving information, validating, saving, reporting), so the level of the function name (saveUser) is high-level and very different from the low-level operations that are performed inside it.

A final point about different levels of operation is that you should not mix different levels of operation together. Consider the following example:

if (!email.includes('@')) {
    console.log('invalid email');
} else {
    const user = new User(email);
    user.save();
}

Here, we have placed the low-level code like email.includes next to the high-level code like user.save, which is not optimal in terms of readability and cleanliness of the code, and other developers should take time to understand it. While we could write the above code as follows:

if (!isEmail(email)) {
    showError('invalid email');
} else {
    saveNewUser(email);
}

In this case, all the operations are at the same level (all are top level) so it is faster and easier for others to read. When we have several different levels of code at the same time, the reader's mind needs to change gears to read this code and must move between these levels. In a simple example like the one above, this is not so obvious, but if you have long codes, then you will be annoyed. You can see this in our main function:

function renderContent(renderInformation) {
  const element = renderInformation.element;
  if (element === 'script' || element === 'SCRIPT') {
    throw new Error('Invalid element.');
  }


let partialOpeningTag = '<' + element;


const attributes = renderInformation.attributes;


for (const attribute of attributes) { partialOpeningTag = partialOpeningTag + ' ' + attribute.name + '="' + attribute.value + '"'; }


const openingTag = partialOpeningTag + '>';


const closingTag = ''; const content = renderInformation.content;


const template = openingTag + content + closingTag;


const rootElement = renderInformation.root;


rootElement.innerHTML = template; }

The problem with this function is not only its size. Some files have hundreds of lines of code, but they are not that difficult to read. The main problem of this function is that there is a very big difference between the level of the function name and the level of the operation performed in it. By simply looking at this code, we realize that we are not doing anything except low-level operations, and doing this amount of low-level operations makes the code unreadable. Now if we look at the modified function, the readability is much better:

function renderContent(renderInformation) {
  const element = renderInformation.element;
  const rootElement = renderInformation.root;


validateElementType(element);


const content = createRenderableContent(renderInformation);


renderOnRoot(rootElement, content); }

In this example, the function name is renderContent, which is a very high-level name, but all the operations performed in it are one level lower than this name, so there is not much distance between them.

It is not easy for everyone to fully learn this abstract topic and it comes with experience, but it is worth thinking about. Even after years of coding and experience, you may still write functions that do not match the explained rules, but the important thing is to try as much as possible and be aware of these rules.

When to break the function?

According to the explained topics, we face an important question: when to break the functions and when to merge them? I will show you two general rules in this regard, but remember that these rules are general and may change depending on your circumstances and needs.

Rule 1: Merge codes that work on the same operation. For example, if you had two functions named user.setAge and user.setName that update the user's age and user's name, respectively, you can merge both into a function named user.update; That is, you can delete the setAge and setName functions and write a new function named update that will do the update work. Example:

user.update({age: 27, name: "albro"})

The second rule: if a certain code needs to be interpreted more than the codes around it, it should be broken in the form of another function. We have seen this rule before in such an example:

if (!email.includes('@')) {
    console.log('invalid email');
} else {
    const user = new User(email);
    user.save();
}

In this code, a function like user.save is simple and clear, but a function like email.includes needs more interpretation. For example, if we had used email domain instead of @, it meant that our site only accepts emails from certain domains. This is the issue of code levels, and I explained that low-level codes such as includes cannot have a specific meaning by default, but must be interpreted to understand their purpose.

Challenge and practice

Up to this part of the discussion, we have shown you several different examples, so now it is time to solve the exercise. I have prepared a simple code for you:

function createUser(email, password) {
  if (!email || !email.includes("@") || !password || password.trim() === "") {
    console.log("invalid input!");
    return;
  }


const user = { email: email, password: password, };


database.insert(user); }

This function is responsible for creating a user and has two parameters. I have no problem with the number of parameters so don't bother optimizing it. If you take a simple look at this function, you will notice that it is not unreadable due to its shortness, but at the same time, it is not written in a clean and standard way. Why? Because the function is doing several different things and we have different levels of code. If you pay attention to the body of this function, you will notice a big difference between the level of the function name and the level of the codes inside it. For example, checking for the presence of @ in an email address does not automatically mean validation, and others who read our code must interpret it to understand this. Also, console.log in this code does not necessarily mean showing an error, but we should interpret from the logged text that the author of the code intended to show the error to the user. All this while database.insert does not need to be interpreted and thought about and what it does is completely clear. I would like you to edit the code above to a standard and readable form and then you can look at my answer.

In the first step, we see that we have different levels of code, and the first example is low-level validation code, so it is better to separate them and transfer them to their own function:

function createUser(email, password) {
  if (!inputIsValid(email, password)) {
    console.log("invalid input!");
    return;
  }


const user = { email: email, password: password, };


database.insert(user); }


function inputIsValid(email, password) { return email && email.includes("@") && password && password.trim() !== ""; }

As you can see, I have defined a function called inputIsValid that takes the email and password and performs validation operations on it. Now all the codes inside inputIsValid have the same level and are one level lower than the function name (inputIsValid). Another low-level operation in the createUser function is to use console.log, so we can remove that as well and put it in its own function:

function createUser(email, password) {
  if (!inputIsValid(email, password)) {
    showErrorMessage("input is invalid!");
    return;
  }


const user = { email: email, password: password, };


database.insert(user); }


function inputIsValid(email, password) { return email && email.includes("@") && password && password.trim() !== ""; }


function showErrorMessage(message) { console.log(message); }

The advantage of doing this, in addition to leveling the code level in the createUser function, is two things:

  • Reuse in all parts of the program: from now on we will have a single mechanism for printing errors that has its own function and we can use it in all different parts of the program.
  • Central edit: If in the future we want to make the display of errors more complicated than a simple log, just go back to this section and use our own complex mechanism instead of console.log. Since all the different parts of the program have used the same function to show the error, we will not need to edit different files and editing the same function will be enough to edit all the error messages in the whole program.

At the end, I look at the database.insert code. You may say that this code is at the same level as other function codes, so there is no need to extract it, and you will be completely right, but in such small cases, we get into taste; That is, another person may say that database.insert is a higher level than showErrorMessage, and his words will also be correct, so the choice is up to you. I decide to break this code as well:

function createUser(email, password) {
  if (!inputIsValid(email, password)) {
    showErrorMessage("input is invalid!");
    return;
  }


saveUser(email, password); }


function inputIsValid(email, password) { return email && email.includes("@") && password && password.trim() !== ""; }


function showErrorMessage(message) { console.log(message); }


function saveUser(email, password) { const user = { email: email, password: password, };


database.insert(user); }

Now, if you look at the createUser function, you will notice that it is more readable and more standard. You might think that we are done, but it is not. Although the createUser function has become more readable, there is still room for improvement. Validation is part of creating a new user, so it's wise to put it inside createUser , but writing every step of it (validating first, then displaying an error) is still lower-level than a function like saveUser , so:

function createUser(email, password) {
  validateInput(email, password);


saveUser(email, password); }


function validateInput(email, password) { if (!inputIsValid(email, password)) { throw new Error("Invalid Input!"); } }


function inputIsValid(email, password) { return email && email.includes("@") && password && password.trim() !== ""; }


function showErrorMessage(message) { console.log(message); }


function saveUser(email, password) { const user = { email: email, password: password, };


database.insert(user); }

As you can see we have defined a new function called validateInput which is responsible for throwing an error. Can you say what we will do with this error? I'll put this code on a larger scale to make it easier for you to understand:

function handleCreateUserRequest(request) {
  try {
    createUser('[email protected]', 'testers');
  } catch (error) {
    showErrorMessage(error.message);
  }
}


function createUser(email, password) { validateInput(email, password);


saveUser(email, password); }


function validateInput(email, password) { if (!inputIsValid(email, password)) { throw new Error('Invalid input!'); } }


function inputIsValid(email, password) { return email && email.includes('@') && password && password.trim() !== ''; }


function showErrorMessage(message) { console.log(message); }


function saveUser(email, password) { const user = { email: email, password: password, };


database.insert(user); }

The handleCreateUserRequest function is where we call the createUser function, so we put it in a try - catch block to catch the possible error thrown and pass its text to showErrorMessage.

DRY rule

Another major benefit of breaking functions into smaller units is that you can use them in other parts of your program. In programming, there is a very important rule known as the DRY rule, which stands for "Don't Repeat Yourself". The important point of this rule is reuse, and based on it, we should not have the same code written several times in different parts of the program.

If you have duplicate codes, it becomes very difficult to manage and maintain them. Imagine that you have repeated a certain logic for recording data in the database in 30 different parts of your code, but now the data structure has changed and this logic must also be edited. In this case, you have to edit your code 30 times, which is extremely annoying and increases the possibility of errors.

One of the best ways to identify this problem is to copy the code. If you find that you are copying code from one part to another part of your program, you should rewrite the code correctly because you are repeating the code.

[hive: @albro]



0
0
0.000
3 comments
avatar

Congratulations!


You have obtained a vote from CHESS BROTHERS PROJECT

✅ Good job. Your post has been appreciated and has received support from CHESS BROTHERS ♔ 💪


♟ We invite you to use our hashtag #chessbrothers and learn more about us.

♟♟ You can also reach us on our Discord server and promote your posts there.

♟♟♟ Consider joining our curation trail so we work as a team and you get rewards automatically.

♞♟ Check out our @chessbrotherspro account to learn about the curation process carried out daily by our team.


🥇 If you want to earn profits with your HP delegation and support our project, we invite you to join the Master Investor plan. Here you can learn how to do it.


Kindly

The CHESS BROTHERS team

0
0
0.000
avatar

Thanks for your contribution to the STEMsocial community. Feel free to join us on discord to get to know the rest of us!

Please consider delegating to the @stemsocial account (85% of the curation rewards are returned).

You may also include @stemsocial as a beneficiary of the rewards of this post to get a stronger support. 
 

0
0
0.000