For the very specific case of initialising an async resource there are several design patterns you can use. Note that these design patterns will not really help with other use cases of asynchronous code.
1. Init function
As you've demonstrated in your own code, this is one way to do it. Basically you have an asynchronous method to initialise your resource. This is similar to jQuery's .ready()
function. There are several ways to write an init function. The most straightforward is probably to accept a callback allowing you to continue with your logic:
class Foo {
init (callback) {
connectToDB().then(db => {
this.db = db;
callback(this);
});
}
}
usage:
let foo = new Foo();
foo.init(async function(){
await foo.save();
});
2. Builder pattern
This design pattern is more common in the Java world and is seen less often in javascript. The builder pattern is used when your object needs complex initialisation. Needing an asynchronous resource is exactly the kind of complexity that lends itself well to the builder pattern:
class Foo {
constructor (db) {
if (typeof db === 'undefined') {
throw new Error('Cannot be called directly');
}
this.db = db;
}
static async build () {
let db = await connectToDB();
return new Foo(db);
}
}
usage:
Foo.build().then(foo => {
foo.save();
});
3. On-demand initialisation / hidden init
This design pattern is useful if your initialisation is messy or complicated and you'd prefer a cleaner API. The idea is to cache the resource and only initialise it when not yet initialised:
class Foo {
constructor () {
this.db = null;
}
db () {
if (this._dbConnection !== null) {
return Promise.resolve(this._dbConnection);
}
else {
return connectToDB().then(db => {
this._dbConnection = db;
return db;
})
}
}
async save (data) {
let db = await this.db();
return db.saveData(data);
}
}
usage:
async function () {
let foo = new Foo();
await foo.save(something); // no init!!
await foo.save(somethingElse);
}
Bonus
If you look back at the init function example you will see that the callback looks kind of like a control structure - kind of like a while()
or if()
. This is one of the killer features of anonymous functions - the ability to create control structures. There are good examples of this in standard javascript such as .map()
and .forEach()
and even good-old .sort()
.
You are free to create asynchronous control structures (the coalan/async and async-q libraries are good examples of this). Instead of:
if( ! (await this.tableExists()) ) { ...
You can write it as:
this.ifTableNotExist(()=>{
return this.createTable();
})
.then(()=>{ ...
possible implementation:
ifTableNotExist (callback) {
return new Promise((ok,err) => {
someAsyncFunction((table) => {
if (!table) ok(callback());
});
});
}
async/await is just one tool in asynchronous programming. And is itself a design pattern. Therefore limiting yourself to async/await limits your software design. Get comfortable with anonymous functions and you will see lots of opportunities for refactoring asynchronous code.
Bonus the 2nd
In the example for the on-demand init pattern the usage example saves two pieces of data sequentially by using await. This was because the code would initialise the db connection twice if we don't wait for it to complete.
But what if we want to speed up the code and perform both saves in parallel? What if we want to do this:
// Parallel:
await Promise.all([
foo.save(something),
foo.save(somethingElse)
]);
What we can do is we can have the .db()
method check if there's a pending promise:
// method to get db connection:
db () {
if (this._dbConnection !== null) {
return Promise.resolve(this._dbConnection);
}
else {
if (this._dbPromise === null) {
this._dbPromise = connectToDB().then(db => {
this._dbConnection = db;
return db;
})
}
return this._dbPromise;
}
}
In fact, since there's no limit to how many times we can call .then()
on a Promise, we can actually simplify that and just cache the promise (don't know why I didn't think of it before):
// method to get db connection:
db () {
if (this._dbPromise === null) {
this._dbPromise = connectToDB();
}
return this._dbPromise;
}