Patterns & Javascript
2017 Edition
Hi, I am Thanasis
- Nearly 50 NPM published packages.
- Over 100 OSS repos.
- A zillion contributions in major and small OSS projects.
- Retired troll.
Patterns
On patterns...
When you trust your patterns and you loyally abide by them, then the only problems you will have will be so big you can't miss them
On Patterns...
- Being opinionated has nothing to do with patterns.
- You should have a strong, pragmatic reason behind every pattern, every choice you make.
- Follow best practices until you know why they are there.
- Non-conformism is a lifestyle, we are doing engineering.
- Patterns are a living, breathing organism that adjusts to its environment.
In Javascript, patterns will save your ass!
Today's Menu
- Module composition & types
- Inheritance Patterns
- Working with Promises
The Module Layout
Module Description
Dependencies
System Deps
NPM Deps
Project Deps
Export Statement
/**
* @fileOverview Create Camp Configuration mutation.
*/
const crypto = require('crypto');
const url = require('url');
const { coroutine } = require('bluebird');
const { GraphQLError } = require('graphql/error');
const {
GraphQLString,
GraphQLNonNull,
} = require('graphql');
const campConfigTypes = require('../types/camp-config.type');
const campConfigModel = require('../models/camp-config.model');
const accountModel = require('../../account/model');
const log = require('../../../logger').getChild(__filename);
const {
mutation,
mutationInputType,
mutationPayloadType,
isAdmin,
} = require('../../../schema/helpers');
const {
RecordIDType,
CurrencyInputType,
} = require('../../../schema/types');
/**
* Create Camp Configuration mutation.
*
* @type {Object}
*/
const mut = module.exports = {};
Spread statements after whole module ones
The Dependencies
- System Dependencies
- NPM Dependencies
- Project Dependencies
/**
* @fileOverview Cors Middleware.
*/
var util = require('util');
var fs = require('fs');
var Promise = require('bluebird');
var _ = require('lodash');
var Middleware = require('./middleware');
var helpers = require('../util/helpers');
1.
2.
3.
Method Definition
/**
* Check if provided viewer can mutate the defined album.
*
* @param {Object} viewer The viewer account object.
* @param {string} albumUuid The album uuid to be mutated.
* @param {string} mutationName A human readable unique name to identify
* the mutation.
* @return {Promise(Object)} A Promise with two keys:
* @param {Object} albumToUpdate The album to be updated.
* @param {Object} albumOwner The album owner.
* @throws {GraphQLError} if no privileges.
*/
albumPrivs.canMutate = Promise.method(function (viewer, albumUuid,
mutationName) {
Google Closure Annotations ~loosely used
https://github.com/google/closure-compiler/wiki/Annotating-JavaScript-for-the-Closure-Compiler
The Module Types
Constructors
Objects
Only 2 types of Modules
Module Types
Constructors
const Cors = module.exports = function(){
Middleware.apply(this, arguments);
};
util.inherits(Cors, Middleware);
Objects
const cors = module.exports = {};
Module Types
Constructor
/**
* The Cors Middleware.
*
* @contructor
* @extends {cc.Middleware}
*/
const Cors = module.exports = function(){
Middleware.apply(this, arguments);
};
util.inherits(Cors, Middleware);
/**
* CORS Middleware
*
* @param {Object} req The request Object.
* @param {Object} res The response Object.
* @param {Function(Error=)} next pass ctrl.
*/
Cors.prototype.allowCrossDomain = function(req, res, next) {...};
Don't freak out! solution incoming...
Module Types
Module
/**
* The Cors Middleware.
*
*/
const cors = module.exports = {};
/**
* CORS Middleware
*
* @param {Object} req The request Object.
* @param {Object} res The response Object.
* @param {Function(Error=)} next pass ctrl.
*/
cors.use = function (req, res, next) { ... };
- If you want state --> Ctor
- If you want inheritance --> Ctor
- Helpers / Util --> Obj
- Enums / Dicts --> Obj
- Config Files --> Obj
When to use which
It's all about the state
Module Inheritance
The classical way
Module Inheritance
The Classical way
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var MiddlewareBase = module.exports = function() {
EventEmitter.apply(this, arguments);
this.foo = 1;
};
util.inherits(MiddlewareBase, EventEmitter);
middleware-base.midd.js
var util = require('util');
var MiddleWareBase = require('./middleware-base.midd');
var Cors = module.exports = function() {
MiddlewareBase.apply(this, arguments);
console.log(this.foo); // 1
};
util.inherits(Cors, MiddlewareBase);
cors.midd.js
Beware of context
Cors.prototype.use = function(req, res, next) {
// good
doSomeAsync(this._handleAsync.bind(this));
// bad
doSomeAsync(this._handleAsync);
};
Do not abuse fat arrow
Cors.prototype.use = function(req, res, next) {
// good
doSomeAsync((res) => {
console.log(res);
});
};
// VERY bad
Cors.prototype.use = (req, res, next) => {
}
function readNews (cb) {
// Bad - context is not required here
fetchNews((news) => {
cb(null, news);
});
}
Beware of performance
Cors.prototype.use = function(req, res, next) {
while(true) { // highly repetitive operation
// good
var self = this;
doSomeAsync(function(result) {
self._handleAsync(result);
});
// bad
doSomeAsync(this._handleAsync.bind(this));
}
};
Shameless Plug Time
CIP!
CIP
Classical Inheritance Pattern at its Best
var EventEmitter = require('events').EventEmitter;
var cip = require('cip');
var CeventEmitter = cip.cast(EventEmitter);
module.exports = CeventEmitter.extend();
Base Constructors using CIP
const cip = require('cip');
const Ctor module.exports = cip.extend();
Base Usage
CIP
Classical Inheritance Pattern at its Best
const cip = require('cip');
const Ctor module.exports = cip.extend();
/** In another module far far away ... */
const Ctor = require('../foo/ctor');
const CtorChild = module.exports = Ctor.extend(function () {
/* Constructor */
});
Inheritance using CIP
CIP
Classical Inheritance Pattern at its Best
const MiddlewareBase = require('./middleware-base.midd');
const Cors = module.exports = MiddlewareBase.extendSingleton(function () {
/* Ctor */
});
/** ... in another module far far away... */
const cors = require('../middleware/cors.midd').getInstance();
cors.use();
Singletons
CIP
Classical Inheritance Pattern at its Best
var MiddlewareBase = require('./middleware-base.midd');
var MiddlewareTraits = require('./middleware-traits.midd');
var Cors = module.exports = MiddlewareBase.extend(function () {
/* Ctor */
});
Cors.mixin(MiddlewareTraits);
Mixins!
var Cors = require('./cors.midd');
var SuperCors = module.exports = Cors.extend(function () {
/* Ctor */
});
Inheritance Goes on...
CIP
/thanpolas/cip
Y U NO USE ES2015 CLASSES?
- Short answer: Lack of async/await.
- Long answer on next chapter about Promises
Working with Promises
2 + 1 ways to create Promises
- Novel Promises
- Chained Promises
- Coroutines 'n Promises
Novel Promises
Promises
const Promise = require('bluebird');
/* ... */
ClipEntity.prototype.fetchVideos = function() {
return new Promise(function(resolve, reject) {
fs.read(file, function(err, res) {
if (err) {
reject(err);
} else {
resolve(res);
}
});
});
};
Typically interfacing with vanilla callbacks
Chained Promises
Promises
const Promise = require('bluebird');
/* ... */
ClipEntity.prototype.showVideos = Promise.method(function() {
return this.fetchVideos();
});
When you are only dealing with other promise-returning methods
Coroutine Promises
Promises
const Promise = require('bluebird');
/* ... */
Foo.prototype.bar = Promise.coroutine(function* () {
const videos = yield this.fetchVideos();
return videos;
});
Thank you ES2015 and `yield`
Why wrapping matters
Promises
Foo.prototype.bar = function () {
throw new Error('Unexpected');
});
const Promise = require('bluebird');
Foo.prototype.bar = Promise.method(function() {
throw new Error('Unexpected');
});
When invoked, kiss your server goodbye!
Everything is under control
Why wrapping matters
Promises
Well... yea... you can't wrap your ES2015 Class methods...
Composing Promises
Promises
return dropboxTokenModel.get(this.uid)
.bind(this)
.then(this._processAccessToken)
.then(this._checkDropboxAccess)
.then(this._startProcess)
.then(this._generateResponse)
.then(this._save)
.then(this._createDownloadJobs)
.then(this._purgeSyncFilesForKafka)
.catch((err) => {
log.error('start() :: Processing Failed:', err);
throw err;
});
Summarize your business logic
Advanced Patterns
Promises
// use a temporary custom context for promise execution chain.
const context = {
albumToUpdate: null,
albumOwner: null,
};
return Promise.resolve(albumModel.Album.fetch(viewer, albumUuid))
.bind(context)
.then(function (albumToUpdate) {
if (!albumToUpdate) {
throw new GraphQLError('Album not found');
}
this.albumToUpdate = albumToUpdate;
return accountModel.Account.fetchById(viewer, albumToUpdate.creator_id);
})
Custom context
Sanitize unknown promises
Return Custom Value
Promises
ClipEntity.prototype._process = function(data) {
var clipProcessEnt = new ClipProcessEnt(data);
return clipProcessEnt.parseFiles()
.bind(clipProcessEnt)
.then(clipProcessEnt.isFileVideo)
.then(clipProcessEnt.storeClip)
.then(clipProcessEnt.storeThumb)
.then(clipProcessEnt.updateDatabase)
.then(clipProcessEnt.removeSourceFiles)
.return(data)
.catch(clipProcessEnt.rollback);
};
Running in Parallel
Promises
ClipEntity.prototype._process = function(data) {
var clipProcessEnt = new ClipProcessEnt(data);
var promises = [];
promises.push(clipProcessEnt.isFileVideo());
promises.push(clipProcessEnt.storeClip());
promises.push(clipProcessEnt.storeThumb());
promises.push(clipProcessEnt.updateDatabase());
return Promise.all(promises)
.catch(clipProcessEnt.rollback);
};
Running in Parallel Throttled
Promises
ClipEntity.prototype._process = function(data) {
var clipProcessEnt = new ClipProcessEnt(data);
var promises = [];
promises.push(clipProcessEnt.isFileVideo());
promises.push(clipProcessEnt.storeClip());
promises.push(clipProcessEnt.storeThumb());
promises.push(clipProcessEnt.updateDatabase());
return Promise.all(promises, {concurrency: 10})
.catch(clipProcessEnt.rollback);
};
Recap
The Patterns employed, is what draws the line between a talented engineer and a copy paste warrior
Thank you
Questions?
Javascript Patterns 2017
By thanpolas
Javascript Patterns 2017
How to author modules in node.js
- 940