1

I'm building a game using Angular which has the following mechanics:

  1. An Angular service checks the game state and requests a required user interaction.
  2. A mediator service creates this request and emits it to the relevant Angular component using a RxJS subject.
  3. A response to this request is awaited in this mediator service, game doesn't go on until request is resolved.
  4. 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.

1 Answers1

2

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.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • I still need derived `Request` objects containing information to be displayed in components. If it's safe to hand the `resolve` function to components, I guess I could create Request instances inside the promise executor, with the resolve function as parameter so it can be used by the respond method: `new Promise(resolve => { mediator.send(new Request(data, resolve)); })`. – Dario Scattolini Sep 08 '21 at 23:58
  • 1
    Yes, exactly - just do `this.respond = resolve` in the constructor and you're good. I mean you could also put the `data` in an object literal, but I'll assume your `class` has some extra methods that are more useful than the promise getter stuff from the code in your question :-) – Bergi Sep 09 '21 at 00:01
  • Thanks for the answer, it definitely simplifies the code and clarifies the problems with deferred objects! – Dario Scattolini Sep 09 '21 at 00:05
  • 1
    As a matter of fact just plain interfaces and object literals will do! – Dario Scattolini Sep 09 '21 at 00:09