Photo by Luca Bravo on Unsplash
Clean Code for Flawless Functions and Methods: Tips to Avoid Bugs | #CleanCode P4
Table of contents
In this article 👨🏽💻, we will be discussing functions and methods, and how they can be used to make code concise, bug-free🕸, and clean. We will cover the different components of a function and how to properly write good functions to save time and effort. We will also discuss the importance of taking care when calling and declaring a function, and how to minimize the number of parameters for a cleaner code💠.
Hello Friends,🙋🏾♂️ I'm chubby, let's start this article. I will read along with you and ask relevant questions on your behalf. If you get stuck anywhere feel free to ask in the comments. Do not come to me.🏃🏾♂️
What have we covered already? 📽️
This is Part4 of the #CleanCode series and we have covered:
General CleanCode good practices in P1
How do Good names Matters in P2
How good comment and code formatting can save your life in P3
If you have not read those articles. Don't worry, they will only haunt👻 you till you do not read them.
Attention 🛑: This article includes extensive code snippets that are essential for comprehending the subject matter. If you become unfocused or experience dizziness, simply skip the code and proceed. Your lack of coding experience may be apparent, but you will soon become accustomed to it. Just keep up with the reading.
Understanding the Concept of Functions🏄🏽♂️
We know, everything in JS is an Object🧊, so a function is an object which executes when we call via passing some argument and expect some output. It is a 'Fun'ction if we consider it like that. For Eg:
function myName(string) { // declaring the function
return "My Name is" + string;
}
myName("Anthony Gonsalvis"); // calling the function 👽
// we get output "My Name is Anthony Gonsalvis" if we console.log it.
if we look🧐 at the components of a function, we see the "function" keyword, function name, parameter, and function object scope which contains the expressions🥓 of the function and return statement. These components defer due to different styles of function declaration and expressions.
Similarly if a function or an object contains a function inside its scope🌡, it is termed a "Method" which works similarly to the function. Still, it serves that object/function where it is declared.
For other nitty-gritty detail 👽, you can go to this StackOverflow link. https://stackoverflow.com/questions/336859/var-functionname-function-vs-function-functionname
So, 🙋🏽♂️Where we should take proper care to make the function clean? Yes, in components of the functions. We can divide them into two parts, viz., in calling the function and in declaring the function.
So,🙋🏽♂️ What should we take care of in calling the function? We should make sure the calling of the function should be clean and easy to understand the calling with its required parameters/ arguments. Its argument order should be clear and short which will be easy to pass as an argument.
And 🙋🏽♂️what about the declaration of the function, which comes first and is more complex than calling? Yes, We should make sure the function declaration is clear✨ and understandable, and use intuitive 🙄names of parameters, variables, methods and the function itself. We should take care of the length of the function which also matters.
Start with Easy Task:- Calling a function:🤳🏾
For clean code, there is a simple rule, 💠Minimize the number of parameters💠 because the parameter for the function is directly proportional to the complexity of the functional intuitiveness. The more arguments in the function more will be the cognitive load on the coder to memorize the order of arguments,🗺 types of arguments and values of the arguments.
//functions/methods // Parameters/ arguments
user.save(); 0 // easy, best option
log(message);
isStatement(true); 1 // also easy, depend name of the function
square(2);
point(2,3);
add(2,3); 2 // not easy, depend on function
saveUser(email, password);
log('hi there', false);
triangle(2,4,1);
calc(5, 10, 'add'); 3 // not easy, require function for idea
login(res,req,next) {...};
const arg = [1,2,3,4,5,6,7];
const newArg = [...arg, 8,9,10];
coord(1,3,23,5);
sortArg(...arg); > 3 // What is This??? OMG 😫
findMax(...newArg);
To reduce cognitive load for coders👨🏽💻, it's best to limit the number of parameters in a function. Ideally, functions should require either no arguments or just one.🤹🏽♂️Otherwise, calling the function requires a deeper understanding of its workings.
However, we can reduce the number of parameters by changing the functional architecture to get the argument as an object or array which we can pass easily. We can make a class 🧗🏾♂️of such a function which requires multiple parameters so that next time we just pass the value assigned to its keys.
saveUser(email, password); // this call will save the user in DB.
// we can change this to
saveUser(newUser); // by outsourcing the parameter where
newUser = {
email: "some@at.com"
password: dummy
}
// we can also create new User class contructor function
const user = new User("some@at.com", dummy);
User.save() // hence we have reduced the paramter to None
// similarly
new User('Rahul', 23, 'myemail@somewhere.com');
// it is easy to understand what we are doing but still we have to go through the class function to inquire for the exact order of passing the argument. For solving the issue, we can do this:
new User({
name: 'Rahul'
age: 23
email: 'myemail@somewhere.com' });
// by doing this, we need not to go to the function, we know what we are passing and we can pass it as a key-value pair which do not require any order. Hence make it more intuitive code.
To optimize your code, consider destructuring🤸🏾♀️ parameters to assign each key to its respective value. Although it may require additional code management, 🤹🏽♂️this process can enhance your comprehension of coding🧐 and elevate it to the next level.🕺🏽 You will be able to grasp every aspect of the code and functions, and never have to say "Don't touch 😫the code! if it is working." even though the number of parameters cannot be reduced.
// we have simple function to compare to number which return a boolean.
function compare(a,b, comparator) {
if (comparator === 'equal') {
return a === b;
} else if (comparator === 'not equal') {
return a !== b;
} else if (comparator === 'greater') {
return a > b;
} else if (comparator === 'smaller') {
return a < b;
}
}
// we can call it like:
const isSmaller = compare(2,6, 'smaller');
const isEqual = compare(1,5, 'equal');
// since we are calling this function right after declaring it, so it seems easy in calling now. But if we need to call it elsewhere after bunch of line of code, then we have to come again to the fucntion to inquire about the sequence of parameter and which number come first etc.
// to solve this issue we can make function like
function compare(comparisonData) {
const {first, second, comparator} = comparisonData;
if (comparator === 'equal') {
return first === second;
} else if (comparator === 'not equal') {
return first !== second;
} else if (comparator === 'greater') {
return first > second;
} else if (comparator === 'smaller') {
return first < second;
}
}
// and call like:
const isSmaller = compare({first: 2, second: 6, comparator: 'smaller'});
// here we do not need to remember the sequece, just pass the key-value pair according to the keys. It is just a map of an object which we can easily pass without a hassle on sequece. Hence we have reduced the 3 argument function to one.
/** I know it looks Horrible, but it is more convenient*/
// Code From: Maximillian's Academind
Must Avoid ✋🏽Output Parameters:
Those parameters do not return any value🤿 but manipulate the functional object. It outputs the result of its work and manipulates the parameter, which is unexpected behaviour.
function createId(user) {
user.id = 'uid1';
}
// function which create the id for the user
const user = {name: 'Max'}; // our user do not have id field
createId(user);
// here it edit our user and add id field for it and save inside the user. We do not want this, we want to createId not addId.
// for that either we pass the value of createId into another const for the user
const id = createId(user);
// or we can change the name of the function to what it actually do... addId
addId(user); // here it will do the expected thing, change the user object. hence here we make clear that we do have the output parameter to avoid the unexpected results.
// or we can also simply make the user Object which have the addId method to pass on and avoid the output parameter
class User {
constructor(name) {
this.name = name;
}
addId() {
this.id = 'uid1';
}
}
const customer = new User('Max');
customer.addId();
console.log(customer);
// Hence customer will contain the name and the generated id without passing as an output parameter. We have reduce the parameter to none. This is the best case as it avoid output parameter as it does not have any parameter at all. 😍
Now let's come to the actual ground of battle where we make the difference.
What should our Functions look😎 like for optimal performance?
When it comes to the Function body, we should consider some important aspects of it:
The function ought to be succinct and compact.🐱🏍
- The function should be small and concise, if it becomes long, shorten it by outsourcing and creating another small function. Multiple🦠 simple functions are better than one big 🤢cluttered function.
Hey🙋🏽♂️Which part of the function to outsource? One sec, That part does exactly one👆🏾 task. For eg:
function renderContent(renderInfo) { const element = renderInfo.element; if (element === 'script' || element === 'SCRIPT') { throw new Error('Invalid DOM element'); } let partialOpeningTag = '<' + element; const attributes = renderInfo.attributes; for( const attribute of attributes ) { partialOpeningTag += ' ' + attribute + '="' + attribute.value + '"'; } const openingTag = partialOpeningTag + '>'; const closingTag = '</' + element + '>'; const content = renderInfo.content; const template = openingTag + content + closingTag; const rootElement = renderInfo.root; rootElement.innerHTML = template; }
This function, create the content from the renderInfo by first validating the element tag, and then creating opening and closing tags with attributes. Place all the content accordingly. Here also we have taken the output parameter,(Where 🤷🏽♂️) but in some cases, we do have to handle such parameters.
The idea function performs a single task with precision🤓
We can see, as our function length gets bigger,🙄 our cognitive load increases and we have to focus 😮more on the actual output of the function. But if we have a small function which does exactly one👆🏾 task, then it will the best scenario for the coder. No doubt it will make the code file bigger but it will reduce the unexpected behaviour of the code.
We can write our function like this:
function renderContent(renderInfo) { const element = renderInfo.element; const rootElement = renderInfo.root; validateElementType(element); const content = createRenderableContent(renderInfo); renderOnRoot(rootElement, content); }
Now our actual function become leaner 💃🏽and more intuitive. With proper function names and parameters, we can easily get an idea of what this function will do.
We do not have to look for the ✈outsourced function codes as this will be the main function which we require the most when we want to render any content. Other codes can be placed below the helping function tags.
function validateElementType(element) { if (element === 'script' || element === 'SCRIPT') { throw new Error('Invalid DOM element'); } } function createRenderableContent(renderInfo) { const tags = createTags( renderInfo.element, renderInfo.attributes ); const template = tags.opening + renderInfo.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 attributesList = ''; for (const attribute of attributes) { attributesList = `${attributesList} ${attribute.name} = "${attribute.value}"`; } return attributesList; } function buildTag(tagInfo) { const element = tagInfo.element; const attributes = element.attributes; const isOpening = tagInfo.isOpening; let tag; if (isOpening) { tag = '</' + element + attributes + '>'; } else { tag = '</' + element + '>'; } return tag; }
I know it will look intimidating 😥after refactoring the function. But when we talk about its usefulness and effectiveness it will be worth doing.🆗 The actual objective of clean code is to make the code more understandable and reduce the cognitive load. Splitting💦 the code into more focused functional code will make our life easier in the long run. 💫We do not have to go through all the code we just need the main function which is used to output the desired result.
However, it does not mean that we make functions for all trivial tasks also like for console.log(). When I said, "Function should do exactly one task", it means the task which starts the task by taking input, does that task with proper procedure and gives the desired output which can be used for another function.
The level of Abstraction 📶decides which operation needs a new function and whether we need to split the code or not.
Function and Abstraction Levels:💈
A function, simple or complex always contains higher and lower levels of methods and operations. Higher-level code includes specific functions or methods and a defined procedure to get the task done. Any function which we write will be considered a higher-level function because we write it for a specific purpose.🏍
Lower-level functions are those functions or built-in methods of the programming language which carry out the operation and give a result. We use these built-in methods and functions to create our higher-order functions. Hence the level of abstraction increase♐ with the use of such low and high level of functions inside of a function.
Higher-level Abstraction Code🛴 | Lower-level Abstraction code🛹 |
function with a descriptive name | APIs of programming language, built-in methods |
eg: isEmailValid(email); | email.includes('@'); |
we don't know how the function is getting output, we are only concerned with the output | we know how the method generates output as we know which method we have used to get the desired output |
We use low-level methods inside higher-level code/ function to get the desired output. | Its ambiguity is higher unless it is used for any specific purpose. Its interpretations are added by the reader by using it in higher-level code |
It is used for a specific purpose and the purpose is defined by its name. | it is also used for specific output but used in different functions where it is used for different use. |
Execution goes from higher to lower code | It remains as it is in execution. It just does its part. |
Level of Abstraction Matters:🚍
Because it interprets🤖 the complexity of the function/code,
it determines the part which spilt across different parts to make consistency in the level of abstraction/complexity,
it defines the name of the function which is based on the abstraction level of the function,
functions should do that's one level of abstraction below their name🤯, means, we should perform only that task inside the function for which it is meant for. If we name the function saveUser(), we do not have to validate it inside this function because saveUser() is already a higher-level code, it should focus on the consolidation of the content which should be saved.
function emailIsValid(email) { return email.includes('@'); } // a higher level code (emailIsValid()) contains lower level code include(), this shows the consistency of the level. but if we start validating it by validation logic, it would add more higher level abstration in the code and hence increase the complexity of the function. // like : function saveUser(email) { if(email.inculdes('@')) {.....} // // ... do somethings } // Here name do not conform with the task for which the function was declared. Function should orchestrate all the steps that are required to save the User
Do not mix🍹 multiple levels of abstraction. In this position, we should split 💦the code into another function which brings consistency🆒 in the level of abstraction.
if(!email.includes('@')) { // Here multiple level of console.log('Invalid email') // abstration used, so we have to } else { // read, understand and interpret the const user = new User (email); // different steps and tasks. user.save() } if(!isEmail(email)) { // here we have split the code to reduce showError('Invalid email!') // the level of abstraction and now we } else { // can easily understand what the function saveNewUser(email) // is doing. we just read the steps. }
So, the function should be split across its level of abstraction. Low-level code like comparison, concatenation, validation, creation of object/array etc should be outsourced and higher-level code should be placed inside the main function which will be used to pass the props or parameters based on the need of the function. We do not go through lower-level codes once our function runs properly.🕳
We create functions as steps🎚 of the higher-level function. We name them accordingly and execute them by calling in the proper place.
Note: It does not mean we strictly follow this convension of writing codes for the functions. Different function require different style of procedure/ steps for the execution. Backend code have different approach for the function call than the frontend code. Hence coders are be advised to follow required/appropriate steps in order to run the code. This Clean Code concept is the way of writing efficient code, which come with practice and experience.
Hence, we can make two rules of thumb when we are in a dilemma about what code to split and from where. These are:
Extract code that works on the same functionality,
Extract code that requires more interpretation than the surrounding code.🤷🏽♂️
function createUser(email, password) { if (!email || // validation part: low level !email.includes('@') || !password || password.trim() === '') { console.log('Invalid email or password'); // error: low level return } const user = { // creating user : high level email: email, password: password, } database.insert(user); // saving in DB: not expected here } // we should extract out same type functionality: 1st rule of thumb // we should database part to give it more interpretation. 2nd rule // we should extract out validation as function is based on creating one. it also require extra interpretation. // Hence we got: function createUser(email, password) { validateInput(email, password); saveUser(email, password); } // low level functions function saveUser(email, password) { const user = { email: email, password: password, } database.insert(user); } function inputIsNotValid(email, password) { return !email || !password || !email.includes('@') || password.trim() === ''; } function validateInput(email, password) { if(inputIsNotValid(email,password)) { throw new Error('Invalid Input'); } }
Abstraction & Reusability:♻
If you remember our Clean Code rule of DRY ( Don't Repeat yourself), the abstraction helps us in this also.🆗 Through the level of abstraction, we can split those code which does repetitive work and increase function length. We can make a separate reusable ♻function which reduces our work and help in clean code. Because Reusability matters💫. Ask React developers 🙋🏽♂️who create tons of reusable components to reduce the code and follow this DRY code.
Reusability comes with very handy benefits like:
It reduces codes
It reduces repetition and error-prone areas
it helps in clean code
if we require to change something in code, we do not have to change all the places where we have done the same thing, but only one place where we have initialized the function/ component. Hence make code bug-resilient and debugging easier. 💃🏽
Do not Overdo the Splitting:🤮
Clean code👨💻 does advise that functions should work for those tasks which they are meant for. But it did not always be the case. In large files📃 where we already have a bunch of functions doing specific tasks, we can't just split our code just for the sake of clean code. We have to refactor the whole code file before going for the change in functions. Our Functions should be clean ✨and pure.
What do you mean by 'pure function'?🤷🏽♂️
Those functions generate expected results whenever we call them. If we call them with the same input, they give the same output every time. And those functions that generate dynamic or different outputs every time, are considered impure functions, as they give unexpected outputs.🏃🏾♂️
function generateId(userName) { // pure function as give same output
const id = "id_" + username; // with same userName
return id;
}
function generateId(userName) { // impure function: random output
const id = username + new Date().toISOString();
return id;
}
function generateId(userName) { // impure function: random output
const id = username + Math.random().toString();
return id;
}
However, impure functions do have their usability. We require them in creating random passwords,🈲 encryptions, ㊙in generating unique IDs etc. Wherever we require uniqueness, we require impure functions. But they should be avoided in other places because they will be prone to bugs.🦠
Impure Functions and Side Effects:🤢
Since impure functions include those codes which can affect the state of the function and program as a whole. Hence we should take caution before using them. The side effects come along with these impure functions because they are the effect of the cause of using impure functions.
What is side effect mean?🤷🏽♂️
A side effect is an operation which does not just act on function inputs and change the function output but instead changes the overall system and program state.
let lastAssignedId;
function generateId(userName) { // it was pure function
const id = "id_" + username;
lastAssignedId = id; // added this variable affect the value of id.
return id; // return id will be different from expected
}
// here lastAssignedID is variable storingthe value before generating one. and change id before returning the new genrated one.
// either we change the variable
// or we cange the name which validate the id first then return new id.
Examples of common Side effect functions or variables:
startSession(user);
sending HTTP request;
console.log(); // it also change the state of the program
sendingMessage(user);
How to avoid unexpected side effects?🤷🏽♂️
- Naming the function to indicate whether the function expects unexpected side effects. eg:
Side-effect: Expected | Side-effect: Unexpected |
saveUser(): error handling requires | isValid() boolean result |
showMessage() | validation functions |
connectToDB() may fail to connect | return functions |
generate, create, like verbal function | built-in functions/methods |
- Move 🛒the side effect part into another block of code, which may be inside a new function inside a try-catch 🗑statement etc. Proper error handling 🤹🏽♂️will make sure that unexpected behaviour due to any reason is handled.
Importance of Unit Testing:🚥
If you consider all your functions clean and easy to understand. Then ask yourself a simple question: Can you test them easily? If your answer is 'Yes' 🎉and you 🎭successfully conduct tests for your functions then it is a clean code. But if the answer is 'No.!'🧿 then you should ask Why?🤷🏽♂️
is your single function do too many things?
they are giving unexpected output?
have too many side effects?
have to write too many tests for a single function.?
Then you should consider splitting your code into smaller ones to test them properly.🎯
Testing not only helps in improving the code, but it also helps in cleaning the code. Through testing your understanding of the function and overall code will improve. Those days are gone when the coder says "do not touch, it's working!". You should have the capability to write the code again in a new style if said so.🙃
Many times coders have to change the codes due to incompatibility🤼♂️ issues, only those coders excel in this field 😎who understand the code better and dare to write it again. They do not fear refactoring and testing their code.
Unit testing and Automated testing is a whole different fields of coding where people pursue excellence.🤩 But every coder should at least know about its basic aspect to explain your code to the tester who is going to write the test code to test your code.🤐
What will be in the next blog?👩🏾💻
If you have read/ listen to this last para, it means you are a true 🏋🏾♀️ dedicated person in coding. I know I have written a very long article on this. But I can't help it. I want to split it into two or three articles but, you know, this is Funtion after all, and we fear working too much on it.
In the next article, I'll be talking about Control Structures👼🏾: loops and if statements, the major pitfall after Function. We discuss how we should structure them to understand the property and get an idea of how they are working. We talk about how to avoid deeply nested loops, work with error handling and how we can utilize errors etc.
Can we see some programming memes.? YESSS🙋🏽♂️ ok...
Follow me on [Linkedin](https://www.linkedin.com/in/rahul-singh-840714254/)
Follow me on [Github](https://github.com/Rahul4dev)