About this presentation
- Focus on practical tips for writing cleaner code
- We'll look at actual before and after code samples
- We'll also highlight tools which can help us write cleaner code
Presentation source at:
http://github.com/cjus/writing-cleaner-js-presentation
Issues with this presentation? Fix it and issue a pull request
---
> 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.
About me
- Carlos Justiniano, HC.JS founder and a long-time programmer
- I'm into full-stack JS development
- I've consulted for numerous companies on commercial JS based products
---
> 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
What is clean code?
- Code that is reasonably understandable
- Code free of side effects
- Code that is testable
---
> 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.
AWARENESS IS HALF THE BATTLE
---
>
Identifying and labeling bad code
- Poorly written code
- Mental context switching and code complexity
- Tech / code debt
---
> 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
Poorly written code
- Very few of us set out to write poor code
- In many environments business pressures stress code quality
- Often clean code degrades to poor code over time
---
> 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.
Mental context switches and code complexity
- In computing a context switch is the basis of multitasking
- In humans, a mental context occurs when you have to switch between various program states as you attempt to understand what a program is doing
- The more complicated the code the more difficult the mental context switches
- Definite correlation between the number of context switches and code complexity
- Takeaway: seek to simplify
---
> 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.
Technical debt ... referring to the eventual consequences of poor system design, software architecture or software development within a codebase. - Wikipedia
---
> 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.
Why should you care?
- Often the first person who has to deal with dirty code is the person who wrote it
- Then others may have to deal - think Karma
- There's a real cost to dealing with dirty code
---
> 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.
Quick tips for writing cleaner code
- Write consistent looking code. Embrace a style guide.
- Take care in naming your variables, functions and objects
- Don't bring your Ruby, Python or {your_language_here} naming conventions - use JS naming conventions... i.e. camelCase, CapitalizeConstructorNames
- Favor small functions and objects
- Limit the number of lines of code in a file or module
---
> 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!
To comment or not to comment?
- It's the subject of some debate
- We can agree that useless comments are worse than no comments
- Con: Comment take work and can get out of date
- Pro: Use comments to share an understanding that can't be easily be inferred from the code
- Pro: Well structured comments can make the code easier to understand by providing visual separators
- Tip: Consider JSDoc (http://usejsdoc.org/)
---
> 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.
Example: JSDoc and Webstorm
/**
* 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;
}
---
>
Don't use magic numbers
- Magic numbers are numbers that appear in code which seem to come from nowhere
//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
Don't use single line if statements
//Before
if (!this.loadedSubViews) { this.loadedSubViews = []; }
//After
if (!this.loadedSubViews) {
this.loadedSubViews = [];
}
---
>
Always include curly braces
//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
Remember variable hoisting
- In JavaScript variable declarations are handled before variable assignments
//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.
Move variable declarations to the top of their function scope
//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.
Avoid single var abuse #1
//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.
Avoid single var abuse #2
//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...
Avoid single var abuse #3
//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.
Avoid single var abuse #4
//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.
You don't have to assign vars right away
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.
TIPS FOR CONDITIONALS
---
>
Avoid large conditional blocks #1
//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.
Avoid large conditional blocks #2
//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.
Move nested functions to the top of the enclosing function after the var declarations
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.
Name your anonymous functions
//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.
Avoid long function chains
//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.
Avoid too many vars in a function
- Too many vars in a function is what we call a code smell
- A sign that things are not quite right in there
- Also consider using arrays
---
> If a function has too many vars then perhaps the function is too large and should be broken up.
Favor passing objects rather than a list of parameters
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 clustering and flow
---
>
Code clustering and flow
- Code clustering is about grouping related code in order to make it easier to work with
- It's about code organization at the declaration stage
- Flow is about organizing program flow so that related functionality executes in a logical and easy to understand manner
---
> 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.
TIPS FOR REFACTORING
---
>
TIPS FOR REFACTORING
- Refactoring is about improving code often without changing what it does
- Refactoring can be error prone if you don't have tests for your code
- This is one of the many important reasons for adding tests to your project
- Code which has tests is easier to refactor, because you can confirm that the code worked before you changed it
- Refactored code should be easier to understand and maintain - if not you're not refactoring you're doing something like perhaps optimizing
---
> 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
Tools for writing cleaner code
- Embrace consistency. Adopt a code style guide.
- Lots to choose from, see this one to start:
https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml
- Use Linters and style checkers. See: JSHint, JSCS
---
>
Contact
- cjus on Twitter and Github
- Email: cjus34@gmail.com
- About: http://cjus.me