-
Notifications
You must be signed in to change notification settings - Fork 0
Coding Guidelines
-
Readability
- Prioritize readability over performance
- Favor declarative over imperative programming
- Avoiding callback hell
- Refactoring
-
Simplicity
- Small is almost always better than large
- Favor pure/stateless over side-effects/stateful functions
- Modularity
-
The Boy Scout Rule
- The Boy Scout Rule
-
Testing
- Why tests are important
-
Code Reviews
- Why code reviews are important
- Etiquette
- Guidelines
-
Adding UI Components
- Module
- Config/Routing
- Component
- Controller
- Factory/Service
- Template
-
Writing APIs in Loopback
- Generating new models
- Writing remote methods
- Writing helper functions
Unless performance is an issue, code readability should always take priority.
A few things that make code more readable:
- Good variable/method names
- Small, composable, self-describing functions
- Avoiding deeply nested conditionals or loops
- Using whitespace where necessary
- Using comments where necessary
- Coding declaratively rather than imperatively
- What else?
Declarative programming tends to be more descriptive, elegant, and robust. See this short article for more reasons why.
As a (contrived) example, let's say we want to calculate the sum of the squares of all the odd integers in an array. We might write something (imperatively) like this:
const calculate = (arr) => {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
let num = arr[i];
// check if the number is odd
if (num % 2 === 1) {
// calculate the square of the number
let squared = num * num;
// add the squared value to the sum
sum = sum + squared;
}
}
return sum;
};
If we wanted to write it more declaratively, it might look something like this:
const isOdd = (x) => x % 2 === 1;
const squared = (x) => x * x;
const sum = (x, y) => x + y;
const calculate = (arr) => {
return arr
.filter(isOdd)
.map(squared)
.reduce(sum);
};
Then, if we wanted to write another function that gets the sum of all the even integers in an array, it's very straightforward:
const isEven = (x) => x % 2 === 0;
// or, using another function:
// const isEven = (x) => !isOdd(x)
const f = (arr) => {
return arr
.filter(isEven)
.reduce(sum);
};
Not only is this more composable, but it is also arguably more readable. The first version, while more performant, is much less intuitive (especially if comments are missing). And readability should always be prioritized above performance until performance becomes an issue.
It's very easy to find ourselves writing code that looks like this:
function fetch(data, cb) {
getSomeStuff(data, (err, stuff) => {
if (err) return cb(err);
getMoreStuff(data, (err, more) => {
if (err) return cb(err);
combineStuff(stuff, more, (err, combined) => {
if (err) return cb(err);
formatStuff(combined, (err, formatted) => {
if (err) return cb(err);
return cb(null, formatted);
});
});
});
});
}
There are a few problems here. First of all, it's aesthetically unappealing. Second, it's not very DRY, given how often we have to write if (err) return cb(err)
. Lastly, it's a very inefficient way to manage the flow of our application.
One solution is to use promises, like so:
function fetch(data, cb) {
const concurrent = [
getSomeStuff(data),
getMoreStuff(data)
];
Promise.all(concurrent)
.then(result => combineStuff(...result))
.then(combined => formatStuff(combined))
.then(formatted => cb(null, formatted))
.catch(err => cb(err));
}
Not only do promises read more nicely, but they allow us to run functions concurrently more effectively (as we do above with getSomeStuff
and getMoreStuff
), as well as simplify error handling.
Alternatively, another powerful solution to callback hell is pairing generator functions with a library like co
, which roughly mimics the behavior of ES7's upcoming async...await
feature.
With co:
function fetch(data, cb) {
co(function* () {
const [stuff, more] = yield [
getSomeStuff(data),
getMoreStuff(data)
];
const combined = yield combineStuff(stuff, more);
const formatted = yield formatStuff(combined);
return cb(null, formatted);
})
.catch(err => cb(err));
}
Always be refactoring. Aim to remove as much code as possible. Code should by DRY ("Don't Repeat Yourself")
Code smells to look out for:
- Repetition
- "Spaghetti code", or code that lacks readability
- Bloated functions (i.e. function definitions greater than 200 lines of code)
- Functions that do more than one thing (should probably be broken down into smaller functions)
- Poor variable naming
- Misplaced logic (i.e. function belongs on a different model)
- Long, nested
if...else
orswitch
statements (can often be replaced with objects or small functions) - Lots of comments to explain what's going on in the code
Recommended reading/skimming:
Small functions and small modules are much more robust and easier to maintain that large, bloated ones. A couple good rules of thumb:
- Functions should do one thing, and one thing only
- Functions should not exceed a certain number of lines in length (for example, no more than 100 lines, but this number is subjective)
Consider:
const sum = (prices) =>
prices.reduce((total, p) => total + p, 0);
const setTotal = (order) => {
order.total = sum(order.prices);
};
const setExpiration = (order) => {
order.expiry = new Date(order.lastDay);
};
const format = (order) => {
setTotal(order);
setExpiration(order);
return order;
};
const order = {
prices: [1, 2, 3],
lastDay: '2020-01-01'
};
const formatted = format(order);
console.log(formatted);
// =>
// { prices: [ 1, 2, 3 ],
// lastDay: '2020-01-01',
// total: 6,
// expiry: 2020-01-01T00:00:00.000Z }
console.log(order); // mutated! :(
// =>
// { prices: [ 1, 2, 3 ],
// lastDay: '2020-01-01',
// total: 6,
// expiry: 2020-01-01T00:00:00.000Z }
Versus:
const sum = (prices) =>
prices.reduce((total, p) => total + p, 0);
const getTotal = (order) => sum(order.prices);
const getExpiration = (order) => new Date(order.lastDay);
const format = (order) => {
return Object.assign({}, order, {
total: getTotal(order),
expiry: getExpiration(order)
});
};
const order = {
prices: [1, 2, 3],
lastDay: '2020-01-01'
};
const formatted = format(order);
console.log(formatted);
// =>
// { prices: [ 1, 2, 3 ],
// lastDay: '2020-01-01',
// total: 6,
// expiry: 2020-01-01T00:00:00.000Z }
console.log(order); // not mutated! :)
// =>
// { prices: [ 1, 2, 3 ], lastDay: '2020-01-01' }
Though it's not always possible to write pure functions, it's a good way to prevent unexpected side-effects. It also makes things much easier to test, since all logic is contained within the function and doesn't rely on state.
Modularity makes for much more composable and maintainable code.
As a contrived example...
A module for math formulas:
/*
* math.js
*/
const average = (x, y, idx) =>
((x * idx) + y) / (idx + 1);
const double = (n) => n * 2;
module.exports = { average, double };
A module for dealing with pixel conversions:
/*
* pixels.js
*/
const toPx = (n) => `${n}px`;
const fromPx = (px) =>
parseInt(px.replace(/px/, ''));
const apply = (fn, px) =>
toPx(fn(fromPx(px)));
module.exports = { toPx, fromPx, apply };
A module for handling dimensions of HTML elements:
/*
* dimensions.js
*/
const math = require('./math'); // import modules
const pixels = require('./pixels'); // as needed :)
const zoom = (dimensions) =>
dimensions.map(px =>
pixels.apply(math.double, px));
const averageSize = (dimensions) =>
dimensions
.map(pixels.fromPx)
.reduce(math.average);
const dimensions = ['10px', '15px', '20px', '25px'];
console.log(zoom(dimensions));
// => [ '20px', '30px', '40px', '50px' ]
console.log(averageSize(dimensions));
// => 17.5
Versus...
One file doing everything:
/*
* main.js
*/
const zoom = (dimensions) => {
return dimensions
.map(px => {
const num = parseInt(px.replace(/px/, ''));
const doubled = num * 2;
return `${doubled}px`;
});
};
const averageSize = (dimensions) => {
const sum = dimensions.reduce((total, px) => {
const num = parseInt(px.replace(/px/, ''));
return total + num;
}, 0);
return sum / dimensions.length;
};
const dimensions = ['10px', '15px', '20px', '25px'];
console.log(zoom(dimensions));
// => [ '20px', '30px', '40px', '50px' ]
console.log(averageSize(dimensions));
// => 17.5
Though the latter example may have fewer lines of code in this case, the former example will scale much better by keeping functions small, simple, and well-organized.
The Boy Scouts have a rule: "Always leave the campground cleaner than you found it."
Uncle Bob (author of Clean Code) writes:
What if we followed a similar rule in our code: "Always check a module in cleaner than when you checked it out." No matter who the original author was, what if we always made some effort, no matter how small, to improve the module. What would be the result?
I think if we all followed that simple rule, we'd see the end of the relentless deterioration of our software systems. Instead, our systems would gradually get better and better as they evolved. We'd also see teams caring for the system as a whole, rather than just individuals caring for their own small little part.
I don't think this rule is too much to ask. You don't have to make every module perfect before you check it in. You simply have to make it a little bit better than when you checked it out. Of course, this means that any code you add to a module must be clean. It also means that you clean up at least one other thing before you check the module back in. You might simply improve the name of one variable, or split one long function into two smaller functions. You might break a circular dependency, or add an interface to decouple policy from detail.
Frankly, this just sounds like common decency to me — like washing your hands after you use the restroom, or putting your trash in the bin instead of dropping it on the floor. Indeed the act of leaving a mess in the code should be as socially unacceptable as littering. It should be something that just isn't done.
But it's more than that. Caring for our own code is one thing. Caring for the team's code is quite another. Teams help each other, and clean up after each other. They follow the Boy Scout rule because it's good for everyone, not just good for themselves.
While everyone has their own opinion on the best way to implement tests, most developers can agree that a certain degree of test coverage is a great way to ensure a higher level of code quality and robustness.
In my opinion the most important thing good test coverage offers is peace of mind when refactoring old code, since refactoring is so essential to maintaining a high level of quality in a code base. I'd say this reason alone should be enough to convince anyone that testing is valuable and speeds up productivity.
Another thing I've encountered when writing tests is that it forces me to think about the problem I'm trying to solve in a different way, and often improves the design of my code.
While code reviews, like testing, are another great way to maintain good code quality, the most valuable aspect of code reviews is in knowledge sharing. For junior developers, reviewing a senior developers code gives them the opportunity to see how good, clean code is written. And for all developers, reviewing other people's code gives them a chance to discuss implementation logic and come up with cleaner, better ways to do things. Also, the more code we review, the more familiar we become with the entire system.
- As the submitter, be open to criticism
- Review your own code before asking someone else for a review
- You are not your code
- But sometimes it's difficult not to feel attached to our work
- So as the reviewer, show some tact
- Don't just criticize; be encouraging and compliment code that looks good
- Indicate which issues are blocking vs non-blocking
- For non-blocking issues or nitpicks, explain your reasoning as much as possible
- Ask questions, try not to make assumptions
- Create a culture of prioritizing code reviews
- Define best practices, like this
Things to check for:
- Code duplication
- General readability
- Function/variable naming
- Error handling
- Bloated functions
- Unnecessary comments, or unused commented out code
- Test coverage (when necessary)
Here's how we might go about adding a blog
module to our Angular app (using an approach inspired by https://github.com/AngularClass/NG6-starter):
This is where we bundle up our components, services, and configuration into a module that can be injected into other parts of our application:
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import config from './config';
import component from './component';
import service from './service';
const blog = angular
.module('blog', [
uiRouter
])
.config(config)
.component('blog', component)
.factory('BlogService', service);
export default blog;
This is where we handle routing and other configurations for the module:
const config = ($stateProvider) => {
$stateProvider
.state('blog', {
url: '/blog',
template: '<blog></blog>'
});
};
config.$inject = ['$stateProvider'];
export default config;
This is where we hook up our template (view) with our controller to create a reusable component:
import template from './template.html';
import controller from './controller.js';
const component = {
template,
controller,
restrict: 'E'
};
export default component;
This is where we handle the view logic (but should avoid putting business logic here, which belongs instead to a service or to the API):
class BlogController {
constructor(BlogService) {
this.BlogService = BlogService;
this.entry = '';
this.entries = [];
}
$onInit() {
return this.fetch();
}
fetch() {
return this.BlogService.list()
.then(entries => {
this.entries = entries;
});
}
add(content) {
return this.BlogService.create({ content })
.then(entry => {
this.entry = '';
this.entries = this.entries.concat(entry);
});
}
remove(id) {
return this.BlogService.delete(id)
.then(entry => {
this.entries = this.entries
.filter(e => e.id !== id);
});
}
}
BlogController.$inject = ['BlogService'];
export default BlogController;
This is where we encapsulate the logic we use in our controllers:
function BlogService($http) {
return {
list() {
return $http.get('/api/entries')
.then(res => res.data.entries);
},
create(params) {
return $http.post('/api/entries', params)
.then(res => res.data.entry);
},
delete(id) {
return $http.delete(`/api/entries/${id}`)
.then(res => res.data.entry);
}
};
};
BlogService.$inject = ['$http'];
export default BlogService;
This is where we design the view for our component:
<section>
<h1>Blog</h1>
<input
type="text"
ng-model="$ctrl.entry" />
<button ng-click="$ctrl.add($ctrl.entry)">
Add
</button>
<ul>
<entry
ng-repeat="ent in $ctrl.entries"
entry="ent"
on-remove="$ctrl.remove(ent.id)" />
</ul>
</section>
To generate a new Loopback model - in this case, a Todo
model - run
slc loopback:model Todo
which will generate the following files:
todo.json
, for the model config
{
"name": "Todo",
"base": "PersistedModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {},
"validations": [],
"relations": {},
"acls": [
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "DENY"
}
],
"methods": {}
}
and todo.js
, for the API methods
'use strict';
module.exports = function(Todo) {
// APIs go here
};
In the remote method, we handle extracting data from the context of the request, and then pass the data we need to another function responsible for handling the bulk of the logic. Also, as a good practice, we allow the method to return a promise if no callback function is specified:
/**
* Fetch all the to-do items for the current user,
* filtered by the user's current team and permissions.
* @param {Function} cb - callback with todo items
* @return {}
*/
Todo.fetchMyTodos = function(cb) {
cb = cb || createPromiseCallback();
const ctx = loopback.getCurrentContext();
const currentUser = ctx.get('currentUser');
const orgId = currentUser.currentOrgId;
const personId = currentUser.id;
fetchAllTodos(orgId, personId)
.then(todos => cb(null, todos))
.catch(err => cb(err));
return cb.promise;
};
Todo.remoteMethod('fetchMyTodos', {
accepts: [],
returns: { arg: 'todos', type: 'array' },
http: { verb: 'get' }
});
This is the main function used to handle the logic of the remote method above, broken down with even more helper functions:
function fetchAllTodos(orgId, personId) {
return Promise.all([
fetchQuoteSelectTodos(orgId, personId),
fetchVoyageSelectTodos(orgId, personId),
fetchDocActionTodos(orgId, personId)
])
.then(flatten);
}
The idea is that each function should do one thing, and one thing only. This makes them both easier to read and easier to test:
function fetchQuoteSelectTodos(orgId, personId) {
// ...
}
function fetchVoyageSelectTodos(orgId, personId) {
// ...
}
function fetchDocActionTodos(orgId, personId) {
// ...
}