Object readiness design pattern
Intent
Wait on asynchronous activity started by the constructor or another method of an object.
Motivation
Objects mutate. Sometimes this mutation begins during construction and continues after an object reference is returned. For example construction or another method may cause state to be loaded from a database server. The constructor or method exits before mutation is complete, and the code following constructor invocation may well execute before the constructed object is ready for use.
Asynchronous functions deal with basically the same problem by returning a Promise in ECMAScript or Task in C#, but this solution cannot be applied to constructors because the value return is the constructed object and therefore cannot be a Promise. Historically this situation is handled by externalising initialisation using a factory method or factory function. The shortcomings of these solutions are evident: they break encapsulation.
What these approaches fail to recognise is that readiness is a property of the object under construction or mutation. A better answer is to give the object a property Ready of type Promise. When the asynchronous mutation completes it must resolve or reject the promise.
Participants
ObjectReadiness
Promise
Collaborations
Client code asks asynchronously constructed objects to call back when ready (see sample code below).
Consequences
Here are key consequences of the ObjectReadiness pattern:
- Code following constructor invocation or other method that puts the object in a non-ready state for an indeterminate period can be made to wait until the object is ready.
- The constructor or method remains asynchronous. It does not block, and code following invocation can execute immediately if it does not depend on the object being ready.
- The use of promise objects rather than simple completion callbacks means that
- Anywhere the object is available, client code can add a callback for execution on readiness.
- Callbacks are executed in the order they are registered.
- Adding a callback can happen at any time irrespective of whether the asynchronous activity is underway or long since finished. If the object is ready the callback will execute immediately.
- Use of a promise rather than a loop relieves calling code of the need to implement waiting logic.
- Use of a promise rather than a callback simplifies error handling.
Implementation
Don't put the object in a promise, put a promise in the object. This is a substantially better arrangement which guarantees that wherever the object is in scope, the promise is also in scope.
In langages that support Promises, implementation is simply a matter of adding a Ready promise property to the class, and using it whenever there is a need to wait on readiness. In the absence of support for promises, a polyfill such as Bluebird will be required.
Sample Code
TypeScript
TypeScript support for the Promise pattern depends on the version of ECMAScript targeted by the compiler. Versions 6 and later have first-class support while earlier versions depend on polyfills such as Bluebird.
class Foo {
public Ready: Promise<any>; // older versions of TS may require Promise.IThenable<any>
constructor(id) {
...
this.Ready = new Promise((resolve, reject) => {
// now do something asynchronous
$.ajax(...).then(result => {
// use result
// at the end of the callback, resolve the readiness promise
resolve(undefined);
}).fail(reject);
});
}
}
var foo = new Foo();
foo.Ready.then(() => {
//do stuff that needs foo to be ready, eg apply bindings
});
A more advanced application waits on the readiness of children in a collection.
class Bar extends Foo {
public Children: Array<Foo> = [];
constructor () {
super();
this.Ready = new Promise((reject,resolve) => {
// asynchronously get data
$.ajax(...).then(result => {
// create instances of Foo into the Children property
// Promise.all still wants IThenable
let thePromisesOfChildren = Children.map(child => child.Ready as Promise.IThenable<any>);
Promise.all(thePromisesOfChildren).then(resolve);
}).fail(reject);
});
}
}
C#
C# directly supports the Promise pattern but calls it Task
.
class Foo {
public Ready: Task;
constructor() {
...
this.Ready = Task.Factory.StartNew(() => {
// use result
resolve(undefined);
});
}
}
}
var foo = new Foo();
foo.Ready.ContinueWith(() => {
//do stuff that needs foo to be ready, eg apply bindings
});
The C# equivalent to Promise.all(Promise[])
is Task.WaitAll(Task[])
.
Known Uses
The first documented example of the Asynchronous Constructor pattern is a blog post concerning asynchonous object construction by Stephen Cleary in ~2012. Peter Wone independently conceived the same solution around the same period. In 2017 in response to a Stack Overflow question he formalised it first as an async constructor pattern and later generalised to the Object Readiness pattern. Widget initialisation depended on both browser DOM readiness and in some cases retrieval of additional data, both asynchronous dependencies. To resolve this, the base class for widgets defined a Ready property and assigned it an already resolved promise, so that it was always safe to write code following widget creation dependent on readiness. Derivatives could assume control of readiness by replacing the value of Ready with a new unresolved promise object.
Prior to this, typical solutions used a factory method with unavoidable violation of encapsulation and no straightforward wait to aggregate dependence.
One school of thought holds that a constructor should never undertake asynchronous operations, with this sort of initialisation implemented in an Init method called later and potentially itself returning a promise. However, this is frequently not a solution that can be retrofitted. There is also the counter-argument that this means that either all classes must always provide an Init method whether this method does something or not, or the developer must check for the existence of an Init method and call it, introducing human error.
2 Comments
Comments have been disabled for this content.
Leo said
Hi,
When I try to use your design pattern for async constructors in typescript, `Promise.IThenable<any>` cannot be recognized by my typescript working environment. Is there any plugs I am missing?
Also, I don't understand what I should put inside the parenthesis of`$.ajax(...)`, it is the instance of the class I am trying to create inside the constructor where ajax function is in, or it is the instance of the constructor itself?
Thank you!
Leo
Peter said
Leo, I suspect you're using a very recent version of Typescript. Try using Promise<any> instead of Promise.IThenable<any>. As for $.ajax(...), that's a sample asynchronous operation using jQuery to use ajax, documented here http://api.jquery.com/jQuery.ajax/