Slides from my talk "Node.js Patterns for Discerning Developers" given at Pittsburgh TechFest 2013. This talk detailed common design pattern for Node.js, as well as common anti-patterns to avoid.
7. Prototype-based Programming
• JavaScript has no classes
• Instead, functions define objects
function Person() {}
var p = new Person();
Image: http://tech2.in.com/features/gaming/five-wacky-gaming-hardware-to-look-forward-to/315742
Prototype
8. Classless Programming
What do classes do for us?
• Define local scope / namespace
• Allow private attributes / methods
• Encapsulate code
• Organize applications in an object-oriented
way
10. Prototype Inheritance
function Person(firstname, lastname){
this.firstname = firstname;
this.lastname = lastname;
}
// Create new class
Employee = Person;//Inherit from superclass
Employee.prototype = {
marital_status: 'single',
salute: function() {
return 'My name is ' + this.firstname;
}
}
var p = new Employee (“Philip”, “Fry”);
11. Watch out!
function Person(firstname, lastname){
this.firstname = firstname;
this.lastname = lastname;
}
// Create new class
Employee = Person;//Inherit from superclass
Employee.prototype = {
marital_status: 'single',
salute: function() {
return 'My name is ' + this.firstname;
}
}
var p = new Employee (“Philip”, “Fry”);
The ‘new’ is very important!
If you forget, your new object will
have global scope internally
12. Another option
function Person(firstname, lastname){
this.firstname = firstname;
this.lastname = lastname;
}
Employee = Person;//Inherit from superclass
Employee.prototype = {
marital_status: 'single',
salute: function() {
return 'My name is ' + this.firstname;
}
}
var p = Object.create(Employee);
p.firstname = 'Philip';
p.lastname = 'Fry';
Works, but you can’t initialize
attributes in constructor
13. Anti-Pattern: JavaScript Imports
• Spread code around files
• Link libraries
• No way to maintain private local
scope/state/namespace
• Leads to:
– Name collisions
– Unnecessary access
14. Pattern: Modules
• An elegant way of encapsulating and
reusing code
• Adapted from YUI, a few years before
Node.js
• Takes advantage of the anonymous
closure features of JavaScript
Image: http://wallpapersus.com/
15. Modules in the Wild
var http = require('http'),
io = require('socket.io'),
_ = require('underscore');
If you’ve programmed in Node, this looks
familiar
16. Anatomy of a module
var privateVal = 'I am Private!';
module.exports = {
answer: 42,
add: function(x, y) {
return x + y;
}
}
mymodule.js
21. Asynchronous Programming
• Node is entirely asynchronous
• You have to think a bit differently
• Failure to understand the event loop and
I/O model can lead to anti-patterns
24. Event Loop
Node.js
Event Loop
The event loop efficiently
manages a thread pool and
executes tasks efficiently…
Thread
1
Thread
2
Thread
n
…
Task 1
Task 2
Task 3
Task 4
Return 1
Callback1()
…and executes each callback as
tasks complete
Node app
25. Async I/O
The following tasks should be done
asynchronously, using the event loop:
• I/O operations
• Heavy computation
• Anything requiring blocking
27. Anti-pattern: Synchronous Code
for (var i = 0; i < 100000; i++){
// Do anything
}
Your app only has one thread, so:
…will bring your app to a grinding halt
28. Anti-pattern: Synchronous Code
But why would you do that?
Good question.
But in other languages (Python), you may do this:
for file in files:
f = open(file, ‘r’)
print f.readline()
29. Anti-pattern: Synchronous Code
The Node.js equivalent is:
Based on examples from: https://github.com/nodebits/distilled-patterns/
var fs = require('fs');
for (var i = 0; i < files.length; i++){
data = fs.readFileSync(files[i]);
console.log(data);
}
…and it will cause severe performance
problems
34. Callback Hell
var db = require('somedatabaseprovider');
//get recent posts
http.get('/recentposts', function(req, res) {
// open database connection
db.openConnection('host', creds,function(err, conn){
res.param['posts'].forEach(post) {
conn.query('select * from users where
id='+post['user'],function(err,users){
conn.close();
res.send(users[0]);
});
}
});
});
35. Callback Hell
var db = require('somedatabaseprovider');
//get recent posts
http.get('/recentposts', function(req, res) {
// open database connection
db.openConnection('host', creds,function(err, conn){
res.param['posts'].forEach(post) {
conn.query('select * from users where
id='+post['user'],function(err,users){
conn.close();
res.send(users[0]);
});
}
});
});
Get recent posts from web
service API
36. Callback Hell
var db = require('somedatabaseprovider');
//get recent posts
http.get('/recentposts', function(req, res) {
// open database connection
db.openConnection('host', creds,function(err, conn){
res.param['posts'].forEach(post) {
conn.query('select * from users where
id='+post['user'],function(err,users){
conn.close();
res.send(users[0]);
});
}
});
});
Open connection to DB
37. Callback Hell
var db = require('somedatabaseprovider');
//get recent posts
http.get('/recentposts', function(req, res) {
// open database connection
db.openConnection('host', creds,function(err, conn){
res.param['posts'].forEach(post) {
conn.query('select * from users where
id='+post['user'],function(err,users){
conn.close();
res.send(users[0]);
});
}
});
});
Get user from DB for each
post
38. Callback Hell
var db = require('somedatabaseprovider');
//get recent posts
http.get('/recentposts', function(req, res) {
// open database connection
db.openConnection('host', creds,function(err, conn){
res.param['posts'].forEach(post) {
conn.query('select * from users where
id='+post['user'],function(err,users){
conn.close();
res.send(users[0]);
});
}
});
});
Return users
39. Callback Hell
var db = require('somedatabaseprovider');
//get recent posts
http.get('/recentposts', function(req, res) {
// open database connection
db.openConnection('host', creds,function(err, conn){
res.param['posts'].forEach(post) {
conn.query('select * from users where
id='+post['user'],function(err,users){
conn.close();
res.send(users[0]);
});
}
});
});
42. Pattern:
Separate Callbacks
fs = require('fs');
callback = function(err,data){
if (err) {
// handle error
}
console.log(data);
}
fs.readFile('f1.txt','utf8',callback);
43. Can Turn This
var db = require('somedatabaseprovider');
http.get('/recentposts', function(req, res){
db.openConnection('host', creds, function(err,
conn){
res.param['posts'].forEach(post) {
conn.query('select * from users where id=' +
post['user'],function(err,results){
conn.close();
res.send(results[0]);
});
}
});
});
44. Into This
var db = require('somedatabaseprovider');
http.get('/recentposts', afterRecentPosts);
function afterRecentPosts(req, res) {
db.openConnection('host', creds, function(err, conn) {
afterDBConnected(res, conn);
});
}
function afterDBConnected(err, conn) {
res.param['posts'].forEach(post) {
conn.query('select * from users where id='+post['user'],afterQuery);
}
}
function afterQuery(err, results) {
conn.close();
res.send(results[0]);
}
46. Pattern: Async.js
Async.js provides common patterns for
async code control flow
https://github.com/caolan/async
Also provides some common functional
programming paradigms
47. Serial/Parallel Functions
• Sometimes you have linear serial/parallel
computations to run, without branching
callback growth
Function
1
Function
2
Function
3
Function
4
Function
1
Function
2
Function
3
Function
4
51. Map
var arr = ['file1','file2','file3'];
async.map(arr, fs.stat, function(err, results){
// results is an array of stats for each file
console.log('File stats: ' +
JSON.stringify(results));
});
52. Filter
var arr = ['file1','file2','file3'];
async.filter(arr, fs.exists, function(results){
// results is a list of the existing files
console.log('Existing files: ' + results);
});
54. Carefree
var fs = require('fs');
for (var i = 0; i < 10000; i++) {
fs.readFileSync(filename);
}
With synchronous code, you can loop as much
as you want:
The file is opened once each iteration.
This works, but is slow and defeats the point of
Node.
55. Synchronous Doesn’t Scale
What if we want to scale to 10,000+
concurrent users?
File I/O becomes
the bottleneck
Users get in a
long line
56. Async to the Rescue
var fs = require('fs');
function onRead(err, file) {
if (err) throw err;
}
for (var i = 0; i < 10000; i++) {
fs.readFile(filename, onRead);
}
What happens if I do this asyncronously?
57. Ruh Roh
The event loop is fast
This will open the file 10,000 times at once
This is unnecessary…and on most systems,
you will run out of file descriptors!
58. Pattern:
The Request Batch
• One solution is to batch requests
• Piggyback on existing requests for the
same file
• Each file then only has one open request
at a time, regardless of requesting clients
59. // Batching wrapper for fs.readFile()
var requestBatches = {};
function batchedReadFile(filename, callback) {
// Is there already a batch for this file?
if (filename in requestBatches) {
// if so, push callback into batch
requestBatches[filename].push(callback);
return;
}
// If not, start a new request
var callbacks = requestBatches[filename] = [callback];
fs.readFile(filename, onRead);
// Flush out the batch on complete
function onRead(err, file) {
delete requestBatches[filename];
for(var i = 0;i < callbacks.length; i++) {
// execute callback, passing arguments along
callbacks[i](err, file);
}
}
}
Based on examples from: https://github.com/nodebits/distilled-patterns/
60. // Batching wrapper for fs.readFile()
var requestBatches = {};
function batchedReadFile(filename, callback) {
// Is there already a batch for this file?
if (filename in requestBatches) {
// if so, push callback into batch
requestBatches[filename].push(callback);
return;
}
// If not, start a new request
var callbacks = requestBatches[filename] = [callback];
fs.readFile(filename, onRead);
// Flush out the batch on complete
function onRead(err, file) {
delete requestBatches[filename];
for(var i = 0;i < callbacks.length; i++) {
// execute callback, passing arguments along
callbacks[i](err, file);
}
}
}
Based on examples from: https://github.com/nodebits/distilled-patterns/
Is this file already being read?
61. // Batching wrapper for fs.readFile()
var requestBatches = {};
function batchedReadFile(filename, callback) {
// Is there already a batch for this file?
if (filename in requestBatches) {
// if so, push callback into batch
requestBatches[filename].push(callback);
return;
}
// If not, start a new request
var callbacks = requestBatches[filename] = [callback];
fs.readFile(filename, onRead);
// Flush out the batch on complete
function onRead(err, file) {
delete requestBatches[filename];
for(var i = 0;i < callbacks.length; i++) {
// execute callback, passing arguments along
callbacks[i](err, file);
}
}
}
Based on examples from: https://github.com/nodebits/distilled-patterns/
If not, start a new file read
operation
62. // Batching wrapper for fs.readFile()
var requestBatches = {};
function batchedReadFile(filename, callback) {
// Is there already a batch for this file?
if (filename in requestBatches) {
// if so, push callback into batch
requestBatches[filename].push(callback);
return;
}
// If not, start a new request
var callbacks = requestBatches[filename] = [callback];
fs.readFile(filename, onRead);
// Flush out the batch on complete
function onRead(err, file) {
delete requestBatches[filename];
for(var i = 0;i < callbacks.length; i++) {
// execute callback, passing arguments along
callbacks[i](err, file);
}
}
}
Based on examples from: https://github.com/nodebits/distilled-patterns/
When read finished, return to
all requests
63. Usage
//Request the resource 10,000 times at once
for (var i = 0; i < 10000; i++) {
batchedReadFile(file, onComplete);
}
function onComplete(err, file) {
if (err) throw err;
else console.log('File contents: ' + file);
}
Based on examples from: https://github.com/nodebits/distilled-patterns/
64. Pattern:
The Request Batch
This pattern is effective on many read-type
operations, not just file reads
Example: also good for web service API
calls
65. Shortcomings
Batching requests is great for high request
spikes
Often, you are more likely to see steady
requests for the same resource
This begs for a caching solution
67. // Caching wrapper around fs.readFile()
var requestCache = {};
function cachingReadFile(filename, callback) {
//Do we have resource in cache?
if (filename in requestCache) {
var value = requestCache[filename];
// Async behavior: delay result till next tick
process.nextTick(function () { callback(null, value); });
return;
}
// If not, start a new request
fs.readFile(filename, onRead);
// Cache the result if there is no error
function onRead(err, contents) {
if (!err) requestCache[filename] = contents;
callback(err, contents);
}
}
Based on examples from: https://github.com/nodebits/distilled-patterns/
68. Usage
// Request the file 10,000 times in series
// Note: for serial requests we need to iterate
// with callbacks, rather than within a loop
var its = 10000;
cachingReadFile(file, next);
function next(err, contents) {
console.log('File contents: ' + contents);
if (!(its--)) return;
cachingReadFile(file, next);
}
Based on examples from: https://github.com/nodebits/distilled-patterns/
69. Almost There!
You’ll notice two issues with the Request
Cache as presented:
• Concurrent requests are an issue again
• Cache invalidation not handled
Let’s combine cache and batch strategies:
70. // Wrapper for both caching and batching of requests
var requestBatches = {}, requestCache = {};
function readFile(filename, callback) {
if (filename in requestCache) { // Do we have resource in cache?
var value = requestCache[filename];
// Delay result till next tick to act async
process.nextTick(function () { callback(null, value); });
return;
}
if (filename in requestBatches) {// Else, does file have a batch?
requestBatches[filename].push(callback);
return;
}
// If neither, create new batch and request
var callbacks = requestBatches[filename] = [callback];
fs.readFile(filename, onRead);
// Cache the result and flush batch
function onRead(err, file) {
if (!err) requestCache[filename] = file;
delete requestBatches[filename];
for (var i=0;i<callbacks.length;i++) { callbacks[i](err, file); }
}
}
Based on examples from: https://github.com/nodebits/distilled-patterns/
71. scale-fs
I wrote a module for scalable File I/O
https://www.npmjs.org/package/scale-fs
Usage:
var fs = require(’scale-fs');
for (var i = 0; i < 10000; i++) {
fs.readFile(filename);
}
72. Final Thoughts
Most anti-patterns in Node.js come from:
• Sketchy JavaScript heritage
• Inexperience with Asynchronous Thinking
Remember, let the Event Loop do the heavy
lifting!
74. Disclaimer
Though I am an employee of the Software
Engineering Institute at Carnegie Mellon
University, this wok was not funded by the
SEI and does not reflect the work or
opinions of the SEI or its customers.