I’m building a game using Angular which has the following mechanics:
- An Angular service checks the game state and requests a required user interaction.
- A mediator service creates this request and emits it to the relevant Angular component using a RxJS subject.
- A response to this request is awaited in this mediator service, game doesn’t go on until request is resolved.
- The component sets the user’s response to the request through a call of
request.respond(response)
method.
I needed to come up with a Request class suitable for this requirements. As requests are resolved once and for all, I decided to avoid basing it on RxJs Observable, and tried using JavaScript Promise instead. Promises can be easly awaited with async
/await
syntax, and requirement (4) led me to find out about the Deferred pattern. I built this base class for all kinds of requests:
abstract class Request<T> { private _resolve: (value: T) => void = () => {}; private _response: Promise<T> = new Promise<T>(resolve => { this._resolve = resolve; }); public get response(): Promise<T> { return this._response; } public respond(response: T) { this._resolve(response); } }
I didn’t add rejection handling since I didn’t come up with a situation when the request could fail. Not even a timeout seems to be needed, since the game requires a response to continue.
This worked perfectly for my purposes, but then I started to find discussions treating this as an anti-pattern (for example,this and this). I’m not used to working with promises, so I don’t fully understand the risks of exposing the resolve function, I can’t discern situations when this pattern would be legitimate, nor can I imagine some other way to meet my requirements using Promise.
I would like to know then if this is a legitimate way to use the Deferred pattern, and in case it is not, if there is another way to achieve what I need.
Advertisement
Answer
The problem of the deferred antipattern is not in exposing the resolve
function in itself, but in exposing it together with (or worse, as part of) the promise. There’s no reason your request class would need to contain the promise. Instead, all you need to do is simply
const response = await new Promise(resolve => { mediator.send({ respond: resolve }); });
The mediator needs nothing but this object, and the component that handles the request can still simply call request.respond(response)
. This is much simpler than doing
const request = new Request(); mediator.send(request); const response = await request.response;
This might be unnecessarily complicated (with all the code in the Request
class), but the usage is not problematic yet. Where it really becomes an antipattern is if you did
function sendRequest() { const request = new Request(); mediator.send(request); return request; }
because now someone has a “deferred object”, not just a promise for the response. They might abuse the function:
const request = sendRequest(); request.respond("Ooops"); const response = await request.response;
This is the actual danger: returning a deferred to code that is not supposed to resolve the promise. It’s totally fine to hand the resolve
function to the component that is supposed to respond though.