On Github cjus / writing-cleaner-js-presentation
Presented by Carlos Justiniano / HC.JS / @cjus / ver: 1.1.0
>
This talk will focus on practical tips for writing cleaner code. However before we get to the practical stuff it's important to set the stage for why this stuff matters. Along the way we'll look at actual before and after code samples and discuss improvements We'll also highlight tools which can help us write cleaner code. This presentation is also open source and available on Github. If you find any issue with this presentation drop me an email or better yet, issue a pull request.
So my name is Carlos Justiniano and I'm the HC.JS founder and a long-time programmer. I'm into full-stack JS development and I've consulted for numerous companies on commercial JS based products I've also worked with small startups and large established companies so I've had a front row seat on the need to produce clean code
So what is clean code? For me, it's code that is reasonably understandable. It's also code that is free from side effects and it's code that can be tested Essentially it's code that I won't regret working with.
>
As we consider ways of writing better code it's important to understand what makes bad code bad and how it leads to tech debt. Poorly written code is the most common one we tend to encounter. The web is full of such code and many of the project we encounter contain poorly written code. That is true more often than not. Aside from poorly written code there is code that is simply complex or more complex than is necessary These factors contribute to something we call tech or code debt. The debt ends up being a cost we pay every time we have to deal with such code. Let's take a closer look
Most of us don't set out to write poor code. Often the pressures of "getting the job done" lead to short cuts and patches which in turn ends up degrading decent code into poor code status.
In computing a context switch is the process of saving the state of a task in order to switch to another task and later return A mental context occurs when you have to switch between various program state in your mind as you attempt to understand what a program is doing The more complicated the code the more difficult the mental context switches And Heaven forbid that you're interrupted during the process! Computers fare well - humans? hmmmm, Not sooo much There is a definite correlation between the number of mental context switches and the complexity of a given code. Naturally this is different for each of us. If a piece of code is unreasonably difficult to work with chances are good that there's room for improvement. And that's the real takeaway, pay attention to how difficult a piece of code is to deal with and consider ways of reducing the complexity - for yourself and for others.
The more difficult a body of code is to deal with the more expensive it may become to maintain. The more poor code we create the more debt we incur. Think of this kind of code as requiring a tax every time someone has to deal with it. This is why it's referred to as a technical debt or code debt.
Often the first person who has to deal with dirty code is the person who wrote it. Then others may have to deal with it over time. Mental context switches lead to fatigue So there's a real cost to dealing with code that is difficult to work with. Anyone who's had to work late evenings and weekends because of difficult code, knows that the cost isn't just financial, it's physical and emotional. Difficult to work with code, enslaves you - it asks you to make sacrifices such as spending less time with your love ones.
>
Embrace a style guide. Which one you choose can be a matter of taste - but pick one - consistency is the real value. Take the time to carefully name your variables, functions and objects. This will make your code easier to understand and maintain. For example, the name "value" is an example of a poor variable name. Use JavaScript established conventions in favor of the conventions from your other favorite language. Keep the size of your functions small. The larger a function the longer it will take to understand and modify it. And the harder it will be to test it. Functions were invented for a reason... use them to refactor your code. The same is true for objects. Limit the number of lines per file or module. I once worked on a bank where a loan application rules engine consisted of a single file with over 10,000 lines of code. Yeah - don't do that!
While this is the subject of some debate, we can all agree that bad or useless code comments are worse than no comments. The reason is because bad comments are form of misinformation. If you can't trust the code then you're worst off. Not adding comments is often just plain laziness - after all is work to create and maintain comments. The goal of comments are not to write a documentary. It's to provide helpful information to travelers. So just as in physical travel, you may not see a lot of signs on the road, but when you do you might tend to take note. Code comments should shed light on areas of code which can't easily be inferred from the actual code. Code comments can also make code easier to understand by visually acting as separators. Consider using JSDoc to make your code easier to understand.
/** * transSingleExecute * Execute a single transaction and handle commit or * rollback, return promise * @param transobj {object} - transaction object * @param statements {array} - transactions statements * @returns {promise|*|Q.promise} */ function transSingleExecute(transobj, statements) { // : return deferred.promise; }
>
//Before var ageInYears = 28, daysOld; daysOld = ageInYears * 365;
//After var ageInYears = 28, daysOld, daysInYear = 365; daysOld = ageInYears * daysInYear;
In the above example 365 is the number of days in a year. So it's best to describe that so it's not left in question
//Before if (!this.loadedSubViews) { this.loadedSubViews = []; }
//After if (!this.loadedSubViews) { this.loadedSubViews = []; }
>
//Before if (!this.loadedSubViews) this.loadedSubViews = [];
//After if (!this.loadedSubViews) { this.loadedSubViews = []; }
Using curly braces makes the code easier to refactor as you add and remove lines of code
>
//Before function foo() { var pi = 3.14159265358979323846264338327950288419716; var radius = 12; console.log('PI, radius: ', pi, radius); var circumference = 2 * pi * radius; console.log('Circumference: ', circumference); }
//After function foo() { var pi; var radius; var circumference; pi = 3.14159265358979323846264338327950288419716; radius = 12; console.log('PI, radius: ', pi, radius); circumference = 2 * pi * radius; console.log('Circumference: ', circumference); }
JavaScript will handle variable declarations before assignments and it will do that behind the scenes. So it's often best to do that in your code because it's what will happen anyway but makes for clear expectations.
//approach #1 function foo() { var pi, radius, circumference; pi = 3.14159265358979323846264338327950288419716; radius = 12; console.log('PI, radius: ', pi, radius); circumference = 2 * pi * radius; console.log('Circumference: ', circumference); }
//approach #2 function foo() { var pi = 3.14159265358979323846264338327950288419716, radius = 12, circumference = 2 * pi * radius; console.log('PI, radius: ', pi, radius); console.log('Circumference: ', circumference); }
Here we see two different approaches for organizing your variables.
//Before function foo() { var pi = 3.14159265358979323846264338327950288419716, radius = 12, circumference = 2 * pi * radius, largerCircleCircumference = circumference * 10; console.log('Larger circle circumference: ', largerCircleCircumference); }
//After function foo() { var pi = 3.14159265358979323846264338327950288419716, radius = 12, circumference, largerCircleCircumference; circumference = 2 * pi * radius, largerCircleCircumference = circumference * 10; console.log('Larger circle circumference: ', largerCircleCircumference); }
A single var statement with multiple declarations can help with variable organization but abusing this feature with a chain of declarations should be avoided.
//Before function foo() { var pi = 3.14159265358979323846264338327950288419716, radius = 12, circumference, largerCircleCircumference; circumference = 2 * pi * radius; largerCircleCircumference = circumference * 10; console.log('Larger circle circumference: ', largerCircleCircumference); }
//adding function declarations function foo() { var pi = 3.14159265358979323846264338327950288419716, radius = 12, circumference, computeLargerCircle = function(factor) { return circumference * 10; }; circumference = 2 * pi * radius; console.log('computeLargerCircle', computeLargerCircle); console.log('Larger circle circumference: ', computeLargerCircle(10)); }
In this code example, we proceed to add the computerLargerCircle function As you can see we can define variables and function using a single var statement Because JavaScript handles this from top down, our computerLargerCircle function can use the circumference variable that was defined earlier Let's abuse this further...
//Not great function foo() { var pi = 3.14159265358979323846264338327950288419716, radius = 12, circumference = function(radius) { return 2 * pi * radius; }, biggerCircle = function() { return circumference(radius) * 2; }; console.log('Bigger circle circumference: ', biggerCircle()); }
So now we continue to define other functions Including a biggerCircle function which in tern uses the circumference function Rather than to declare all of your functions this way you're better off creating an object.
//Not great function foo() { var pi = 3.14159265358979323846264338327950288419716, radius = 12, circumference = function(radius) { return 2 * pi * radius; }, biggerCircle = function() { return circumference(radius) * 2; }, evenBiggerCircle = biggerCircle() * 2; console.log('Bigger circle circumference: ', biggerCircle()); console.log('Even bigger circle circumference: ', evenBiggerCircle); }
If you can avoid it don't declare more variables after the function declarations. In our example above moving the evenBiggerCircle variable declaration and assignment above the biggerCircle function results in an error because biggerCircle wouldn't be defined and available until later.
function foo() { var pi, radius, evenBiggerCircle; function circumference(radius) { return 2 * pi * radius; } function biggerCircle() { return circumference(radius) * 2; } pi = 3.14159265358979323846264338327950288419716; radius = 12; evenBiggerCircle = biggerCircle() * 2; console.log('Bigger circle circumference: ', biggerCircle()); console.log('Even bigger circle circumference: ', evenBiggerCircle); }
Don't forget that you don't have to assign a value to a declared variable. This can help keep your chained var statements maintainable.
>
//Before var getUserIDsToNotify = function(notifyTo, data) { 'use strict'; var userIDs = []; var deferred = q.defer(); if (notifyTo === 'USERS') { if (!data.user_ids) { deferred.reject({success: false}); } else { userIDs = data.user_ids; deferred.resolve(userIDs); } } else if (notifyTo === 'GROUP') { if (!data.group_id) { deferred.reject({success: false}); } else { groupLib.listGroupMembers(data.group_id) .then(function(results) { for (var index in results) { userIDs.push(results[index].user_id); } deferred.resolve(userIDs); }) .catch(function(error) { deferred.reject(error); }); } } else if (notifyTo === 'ALL_GROUPS') { // get all group ids and then users OR get all the users groupLib.listAllGroupMembers() .then(function(results) { for (var index in results) { userIDs.push(results[index].user_id); } deferred.resolve(userIDs); }) .catch(function(error) { deferred.reject(error); }); } return deferred.promise; };
Avoid large conditional blocks. Consider breaking up the body of a large conditional into functions. This can help improve readability.
//After function handleUsers(data, deferred) { var userIDs = []; if (!data.user_ids) { deferred.reject({success: false}); } else { userIDs = data.user_ids; deferred.resolve(userIDs); } } function handleGroup(data, deferred) { var userIDs = []; if (!data.group_id) { deferred.reject({success: false}); } else { groupLib.listGroupMembers(data.group_id) .then(function(results) { for (var index in results) { userIDs.push(results[index].user_id); } deferred.resolve(userIDs); }) .catch(function(error) { deferred.reject(error); }); } } function handleAllGroups(data, deferred) { var userIDs = []; groupLib.listAllGroupMembers() .then(function(results) { for (var index in results) { userIDs.push(results[index].user_id); } deferred.resolve(userIDs); }) .catch(function(error) { deferred.reject(error); }); } function getUserIDsToNotify(notifyTo, data) { 'use strict'; var deferred = q.defer(); if (notifyTo === 'USERS') { handleUsers(data, deferred); } else if (notifyTo === 'GROUP') { handleGroup(data, deferred); } else if (notifyTo === 'ALL_GROUPS') { handleAllGroups(data, deferred); } return deferred.promise; };
Avoid large conditional blocks. Consider breaking up the body of a large conditional into functions. This can help improve readability.
>
function foo() { var pi, radius, evenBiggerCircle; function circumference(radius) { return 2 * pi * radius; } function biggerCircle() { return circumference(radius) * 2; } pi = 3.14159265358979323846264338327950288419716; radius = 12; evenBiggerCircle = biggerCircle() * 2; console.log('Bigger circle circumference: ', biggerCircle()); console.log('Even bigger circle circumference: ', evenBiggerCircle); }
Move nested functions to the top of the enclosing function after the var declarations We saw this in an earlier code example. This is cleaner because as we're reading the code we can clearly see the variable followed by a list of nested functions before we look at how variables are assigned and functions invoked.
//Before setTimeout(function() { console.log("Hello") }, 3000); setTimeout(function() { console.log("World") }, 1000);
//After setTimeout(function Hello() { console.log("Hello") }, 3000); setTimeout(function World() { console.log("World") }, 1000);
Anonymous functions are functions which don't have names. In the before example we have two functions which use a timer to output Hello World. The problem is that World will be outputted before Hello In a more complex example it might be difficult to understand what's going on when you look at this code in a debugger. The reason is because the functions don't have names. In the second example we would be able to see the debugger call stack and now which function was called when. Naming anonymous functions can make it easier to debug more complex code.
//Not great function foo() { var pi = 3.14159265358979323846264338327950288419716, radius = 12, circumference = function(radius) { return 2 * pi * radius; }, biggerCircle = function() { return circumference(radius) * 2; }, evenBiggerCircle = biggerCircle() * 2; console.log('Bigger circle circumference: ', biggerCircle()); console.log('Even bigger circle circumference: ', evenBiggerCircle); }
This one is from an earlier example. Avoid defining functions in long var chains. Imagine a function which defines nested functions each of which has several dozen lines of code. That would be difficult to work with. Instead don't be afraid to pull the functions into their own stand-alone declaration.
If a function has too many vars then perhaps the function is too large and should be broken up.
function drawBanner(text, x, y, color, fontSize, underline, bold) {}
//Usage drawBanner('Hello World', 100,100, 24, false, true);
//After var helloWorldBanner; function drawBanner(banner) {} helloWorldBanner = { text: 'Hellow World', x: 100, y: 100, fontSize: 24, underline: false, bold: true; }; drawBanner(helloWorldBanner);
Consider this first example. We have a function called drawBanner which accepts seven parameters A usage might be something like this... Rather than create functions with a half dozen or more parameters it's cleaner to pass objects into functions
>
Code organization Where possible favor clustering common code. Don't have code all over a file without rhyme or reason. Let's consider this story telling analogy: Say you're telling a story and as you describe the details you present the details in whatever order comes to mind. Oh yes and then we bumped into Suzan... oh hold up - did I tell you we also ran into Gerold? Gerold got a new car by the way. So where was I? Yes Suzan. Try to tell a story with the code you write. Orgainize it so it makes sense and sequence of operations flow logically. Remember the next person who reads your code, might just be you six months later.
>
Refactoring is the process of taking existing code and modifying it so that it you improve it without necessarily changing how it works You can use the ideas in this presentation to provide you with refactoring targets Make sure you have test for your code. Lack of tests can make refactoring error prone and frustrating Refactored code should be easier to understand and maintain - if not you're not refactoring you're doing something like perhaps optimizing
>