async konstruktørfunksjoner i TypeScript?


Jeg har noen oppsett jeg ønsker under en konstruktør, men det ser ut til at det ikke er tillatt

Noe som betyr at jeg ikke kan bruke:

Hvordan ellers skal jeg gjøre dette?

Akkurat nå har jeg noe utenfor som dette, men dette er ikke garantert å kjøre i den rekkefølgen jeg ønsker?

async function run() {
  let topic;
  debug("new TopicsModel");
  try {
    topic = new TopicsModel();
  } catch (err) {
    debug("err", err);
  }

  await topic.setup();

Svar #1

En konstruktør må returnere en instans av klassen den 'konstruerer'. Derfor er det ikke mulig å returnere Promise<...> og vente på den.

Du kan:

  1. Gjør din offentlige oppsett async.

  2. Ikke kall det fra konstruktøren.

  3. Kall det når som helst for å 'fullføre' objektkonstruksjon.

    async function run() 
    {
        let topic;
        debug("new TopicsModel");
        try 
        {
            topic = new TopicsModel();
            await topic.setup();
        } 
        catch (err) 
        {
            debug("err", err);
        }
    }
    

Svar #2

Klare-til-aksjon designmønster

Det har blitt hevdet at

En konstruktør må returnere en instans av klassen den 'konstruerer'. Derfor er det ikke mulig å returnere Promise<...> og vente på det.

Dette er ikke sant. Det er ikke noe krav om at det nevnte løftet skal være roten til objektgrafen som returneres av konstruktøren.

Ikke sett objektet i et løfte, sett et løfte i objektet.

Beredska er en egenskap ved objektet. Så gjør det til en egenskap ved objektet.

Den ventbare initialise-metoden beskrevet i det aksepterte svaret har en alvorlig begrensning. Å bruke await direkte på konstruksjonsuttrykket på denne måten betyr at bare utførelsesbanen kan være implisitt avhengig av at objektet er klart. Dette er greit for kode med garantert lineær utførelse, men i flertrådet eller hendelsesdrevet kode er det uholdbart.

Du kan fange opp oppgaven/løftet og vente på det, men hvordan håndterer du å gjøre dette tilgjengelig for hver kontekst som er avhengig av det?

Problemet er mer håndterbart når det er korrekt definert. Målet er ikke å vente på konstruksjon, men å vente på beredskap av det konstruerte objektet. Dette er to helt forskjellige ting. Det er til og med mulig for noe som et databaseforbindelsesobjekt å være i en klar tilstand, gå tilbake til en ikke-klar tilstand, og deretter bli klart igjen.

Hvordan kan vi avgjøre beredskap hvis det avhenger av aktiviteter som kanskje ikke er fullført når konstruktøren returnerer? Nokså åpenbart er beredskap en egenskap ved objektet. Mange rammeverk uttrykker direkte begrepet beredskap. I JavaScript har vi Promise, og i C# har vi Task. Begge har direkte språklig støtte for objekt-egenskaper.

Eksponer konstruksjonens fullføringsløfte som en egenskap på det konstruerte objektet. Når den asynkrone delen av konstruksjonen din er ferdig, skal den løse løftet.

Det spiller ingen rolle om .then(...) kjører før eller etter at løftet blir oppfylt. Spesifikasjonen for løfter sier at å påkalle then på et allerede oppfylt løfte ganske enkelt kjører håndtereren umiddelbart.

class Foo {
  public Ready: Promise.IThenable<any>;
  constructor() {
    ...
    this.Ready = new Promise((resolve, reject) => {
      $.ajax(...).then(result => {
        // use result
        resolve(undefined);
      }).fail(reject);
    });
  }
}

var foo = new Foo();
foo.Ready.then(() => {
  // do stuff that needs foo to be ready, eg apply bindings
});
// keep going with other stuff that doesn't need to wait for foo

// using await
// code that doesn't need foo to be ready
await foo.Ready;
// code that needs foo to be ready

Hvorfor resolve(undefined); i stedet for resolve();? Fordi ES6. Juster etter behov for å passe til ditt mål.

Bruke await

I en kommentar har det blitt foreslått at jeg skulle ha rammet inn denne løsningen med await for å mer direkte ta opp spørsmålet som ble stilt.

Du kan bruke await med Ready-egenskapen som vist i eksemplet ovenfor. Jeg er ikke så glad i await fordi det krever at du deler opp koden din etter avhengighet. Du må plassere all avhengig kode etter await og all uavhengig kode før den. Dette kan skjule hensikten med koden.

Jeg oppfordrer folk til å tenke i form av tilbakekallinger. Å mentalt ramme inn problemet på denne måten er mer kompatibelt med språk som C. Løfter er sannsynligvis avledet fra mønsteret som brukes for IO-fullføring.

Mangel på håndhevelse sammenlignet med fabrikkmønsteret

En spiller mener at dette mønsteret "er en dårlig idé fordi uten en fabrikkfunksjon er det ingenting som håndhever regelen om å sjekke om noe er klart. Det overlates til klientene, som du praktisk talt kan garantere vil gjøre feil fra tid til annen."

Hvis du tar denne posisjonen, hvordan vil du da hindre folk i å lage fabrikkmetoder som ikke håndhever sjekken? Hvor trekker du grensen? For eksempel, ville du forby divisjonsoperatoren fordi det ikke finnes noe som hindrer folk i å bruke null som divisor? Den harde sannheten er at du må lære forskjellen mellom domene-spesifikk kode og rammeverkskode og anvende forskjellige standarder, krydret med litt sunn fornuft.


Forløpere

Dette er originalt arbeid av meg. Jeg utviklet dette designmønsteret fordi jeg var misfornøyd med eksterne fabrikker og andre lignende løsninger. Til tross for at jeg lette en stund, fant jeg ingen tidligere eksempler på min løsning, så jeg hevder kredit som opphavsperson til dette mønsteret inntil det eventuelt bestrides.

Likevel oppdaget jeg i 2020 at Stephen Cleary i 2013 la ut en svært lignende løsning på problemet. Når jeg ser tilbake på mitt eget arbeid, dukker de første sporene av denne tilnærmingen opp i kode jeg jobbet med omtrent på samme tid. Jeg mistenker at Cleary satte det hele sammen først, men han formaliserte det ikke som et mønster eller publiserte det der det lett kunne bli funnet av andre med problemet. Videre omhandler Cleary bare konstruksjon, som bare er én anvendelse av Readiness-mønsteret (se nedenfor).

Sammendrag

Mønsteret er

  • sett et løfte i objektet det beskriver
  • eksponer det som en egenskap kalt Ready
  • referer alltid til løftet via Ready-egenskapen (ikke fang det i en klientkodevariabel)

Dette etablerer klare, enkle semantikker og garanterer at

  • løftet vil bli opprettet og administrert
  • løftet har identisk omfang som objektet det beskriver
  • Semantikken til avhengighet av beredskap er tydelig og klar i klientkoden
  • hvis løftet blir erstattet (f.eks. en tilkobling går fra uferdig til ferdig igjen på grunn av nettverksforhold) vil klientkoden som refererer til det via thing.Ready alltid bruke det nåværende løftet

Denne siste er et mareritt helt til du bruker mønsteret og lar objektet håndtere sitt eget løfte. Det er også en veldig god grunn til å avstå fra å fange løftet i en variabel.

Noen objekter har metoder som midlertidig setter dem i en ugyldig tilstand, og mønsteret kan brukes i det scenariet uten endring. Kode av formen obj.Ready.then(...) vil alltid bruke hvilken som helst løfteegenskap som returneres av Ready-egenskapen, så når en handling er i ferd med å ugyldiggjøre objektets tilstand, kan et nytt løfte opprettes.

Avsluttende merknader

Readiness-mønsteret er ikke spesifikt for bygg og anlegg. Det kan enkelt anvendes i bygg, men det handler egentlig om å sikre at tilstandsendringer er oppfylt. I disse dager med asynkron kode trenger du et system, og den enkle deklarative semantikken til et løfte gjør det enkelt å uttrykke ideen om at en handling bør tas så snart som mulig, med vekt på mulig. Når du begynner å ramme inn ting på denne måten, blir argumenter om langvarige metoder eller konstruktører irrelevante.

Utsatt initialisering har fortsatt sin plass; som jeg nevnte kan du kombinere Readiness med lazy load. Men hvis sjansen er at du ikke vil bruke objektet, hvorfor lage det tidlig? Det kan være bedre å lage det på forespørsel. Eller kanskje ikke; noen ganger kan man ikke tolerere forsinkelse mellom gjenkjennelsen av behovet og oppfyllelsen.

Det finnes mer enn én måte å flå en katt på. Når jeg skriver innebygd programvare, lager jeg alt på forhånd, inkludert ressursbassenger. Dette gjør lekkasjer umulige og minnebehovene kjente ved kompileringstid. Men det er bare en løsning for et lite, lukket problemområde.

Svar #3

Bruk i stedet en asynkron fabrikksmetode.

class MyClass {
   private mMember: Something;

   constructor() {
      this.mMember = await SomeFunctionAsync(); // error
   }
}

Blir :

class MyClass {
   private mMember: Something;

   // make private if possible; I can't in TS 1.8
   constructor() {
   }

   public static CreateAsync = async () => {
      const me = new MyClass();
      
      me.mMember = await SomeFunctionAsync();

      return me;
   };
}

Dette vil bety at du må vente på byggingen av denne typen objekter, men det bør allerede være underforstått av det faktum at du befinner deg i en situasjon hvor du uansett må vente på noe for å konstruere dem.

Det er en annen ting du kan gjøre, men jeg mistenker at det ikke er en god idé:

// probably BAD
class MyClass {
   private mMember: Something;

   constructor() {
      this.LoadAsync();
   }

   private LoadAsync = async () => {
      this.mMember = await SomeFunctionAsync();
   };
}

Dette kan fungere, og jeg har aldri hatt et faktisk problem med det før, men det virker farlig for meg, siden objektet ditt ikke faktisk vil være fullstendig initialisert når du begynner å bruke det.

En annen måte å gjøre det på, som på noen måter kan være bedre enn det første alternativet, er å vente på delene, og deretter konstruere objektet ditt etterpå:

export class MyClass {
   private constructor(
      private readonly mSomething: Something,
      private readonly mSomethingElse: SomethingElse
   ) {
   }

   public static CreateAsync = async () => {
      const something = await SomeFunctionAsync();
      const somethingElse = await SomeOtherFunctionAsync();

      return new MyClass(something, somethingElse);
   };
}

Svar #4

Jeg har funnet en løsning som ser ut som

export class SomeClass {
  private initialization;

  // Implement async constructor
  constructor() {
    this.initialization = this.init();
  }

  async init() {
    await someAsyncCall();
  }

  async fooMethod() {
    await this.initialization();
    // ...some other stuff
  }

  async barMethod() {
    await this.initialization();
    // ...some other stuff
  }

Det fungerer fordi løfter som driver async/await, kan løses flere ganger med samme verdi.

Svar #5

Bruk en privat konstruktør og en statisk fabrikkmetode FTW. Det er den beste måten å håndheve all valideringslogikk eller databerikelse på, innkapslet borte fra en klient.

class Topic {
  public static async create(id: string): Promise<Topic> {
    const topic = new Topic(id);
    await topic.populate();
    return topic;
  }

  private constructor(private id: string) {
    // ...
  }

  private async populate(): Promise<void> {
    // Do something async. Access `this.id` and any other instance fields
  }
}

// To instantiate a Topic
const topic = await Topic.create('1234');

Svar #6

Jeg vet at det er ganske gammelt, men et annet alternativ er å ha en fabrikk som vil lage objektet og vente på at det blir initialisert:

// Declare the class
class A {

  // Declare class constructor
  constructor() {

    // We didn't finish the async job yet
    this.initialized = false;

    // Simulates async job, it takes 5 seconds to have it done
    setTimeout(() => {
      this.initialized = true;
    }, 5000);
  }

  // do something usefull here - thats a normal method
  useful() {
    // but only if initialization was OK
    if (this.initialized) {
      console.log("I am doing something useful here")

    // otherwise throw an error which will be caught by the promise catch
    } else {
      throw new Error("I am not initialized!");
    }
  }

}

// factory for common, extensible class - that's the reason for the constructor parameter
// it can be more sophisticated and accept also params for constructor and pass them there
// also, the timeout is just an example, it will wait for about 10s (1000 x 10ms iterations
function factory(construct) {

  // create a promise
  var aPromise = new Promise(
    function(resolve, reject) {

      // construct the object here
      var a = new construct();

      // setup simple timeout
      var timeout = 1000;

      // called in 10ms intervals to check if the object is initialized
      function waiter() {
    
        if (a.initialized) {
          // if initialized, resolve the promise
          resolve(a);
        } else {

          // check for timeout - do another iteration after 10ms or throw exception
          if (timeout > 0) {     
            timeout--;
            setTimeout(waiter, 10);            
          } else {            
            throw new Error("Timeout!");            
          }

        }
      }
  
      // call the waiter, it will return almost immediately
      waiter();
    }
  );

  // return promise of the object being created and initialized
  return a Promise;
}


// this is some async function to create object of A class and do something with it
async function createObjectAndDoSomethingUseful() {

  // try/catch to capture exceptions during async execution
  try {
    // create object and wait until its initialized (promise resolved)
    var a = await factory(A);
    // then do something usefull
    a.useful();
  } catch(e) {
    // if class instantiation failed from whatever reason, timeout occured or useful was called before the object finished its initialization
    console.error(e);
  }

}

// now, perform the action we want
createObjectAndDoSomethingUsefull();

// spaghetti code is done here, but async probably still runs

Relaterte artikler