async-rakentajafunktiot TypeScriptissä?


Minulla on joitain asetuksia, jotka haluan suorittaa konstruktorin aikana, mutta näyttää siltä, että se ei ole sallittua

Mikä tarkoittaa, etten voi käyttää:

Miten muuten minun pitäisi tehdä tämä?

Tällä hetkellä minulla on jotain tällaista ulkopuolella, mutta tämä ei ole taattu toimivan haluamassani järjestyksessä?

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

  await topic.setup();

Vastaus #1

Rakentajan on palautettava sen luokan instanssi, jota se 'rakentaa'. Siksi ei ole mahdollista palauttaa Promise<...> ja odottaa sitä.

Voit:

  1. Tee julkisesta asetuksestasi async.

  2. Älä kutsu sitä konstruktorista.

  3. Kutsu sitä milloin tahansa haluat 'viimeistelläksesi' objektin rakentamisen.

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

Vastaus #2

Valmius-suunnittelumalli

On väitetty, että

Konstruktori on palautettava luokan instanssi, jota se 'rakentaa'. Siksi ei ole mahdollista palauttaa Promise<...> ja odottaa sitä.

Tämä ei ole totta. Kyseiselle lupaukselle ei ole vaatimusta olla konstruktorin palauttaman objektipuun juuressa.

Älä laita objektia lupaukseen, laita lupaus objektiin.

Valmius on objektin ominaisuus. Joten tee siitä objektin ominaisuus.

Hyväksytyssä vastauksessa kuvattu odotettavissa oleva initialise-metodi sisältää vakavan rajoituksen. Käyttämällä await suoraan konstruktiolausetta tällä tavalla tarkoittaa, että vain yksi suorituspolku voi olla implisiittisesti riippuvainen siitä, että objekti on valmis. Tämä on ok koodille, jonka suoritus on taattu lineaariseksi, mutta monisäikeisessä tai tapahtumapohjaisessa koodissa se on kestämätöntä.

Voisit taltioida tehtävän/lupauksen ja odottaa sitä, mutta kuinka hallitset sen tekemisen saataville jokaisessa kontekstissa, joka riippuu siitä?

Ongelma on helpommin hallittavissa, kun se on oikein muotoiltu. Tavoitteena ei ole odottaa rakentamista, vaan odottaa rakennetun objektin valmiutta. Nämä ovat kaksi täysin eri asiaa. On jopa mahdollista, että esimerkiksi tietokantayhteysobjekti voi olla valmiustilassa, palata ei-valmiustilaan ja sitten tulla taas valmiiksi.

Miten voimme määrittää valmiuden, jos se riippuu toiminnoista, jotka eivät välttämättä ole valmiita rakentajan palattua? Valmius on selvästi kohteen ominaisuus. Monet viitekehykset ilmaisevat suoraan valmiuden käsitteen. JavaScriptissä meillä on Promise, ja C#:ssa Task. Molemmissa on suora kielituki objektien ominaisuuksille.

Paljasta rakennuksen valmistumislupaus rakennettavan objektin ominaisuutena. Kun rakentamisen asynkroninen osa päättyy, sen pitäisi ratkaista lupaus.

Ei ole väliä, suorittuuko .then(...) ennen vai jälkeen lupauksen täyttymisen. Lupausmääritelmä sanoo, että kutsumalla then jo täyttyneeseen lupaukseen suoritetaan käsittelijä yksinkertaisesti heti.

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

Miksi resolve(undefined); eikä resolve();? Koska ES6. Säädä tarvittaessa vastaamaan kohdetta.

Käyttämällä await

Kommentissa on ehdotettu, että minun olisi pitänyt kietoa tämä ratkaisu await-avainsanalla käsitelläkseni kysymystä suoremmin sen mukaan, miten se esitettiin.

Voit käyttää await-avainsanaa Ready-ominaisuuden kanssa kuten yllä olevassa esimerkissä. En ole suuri await-fanin, koska sen käyttö vaatii koodin jakamisen riippuvuuden mukaan. Sinun täytyy sijoittaa kaikki riippuvainen koodi await-avainten jälkeen ja kaikki riippumaton koodi ennen sitä. Tämä voi hämärtää koodin tarkoitusta.

Kannustan ihmisiä ajattelemaan takaisinkutsujen näkökulmasta. Ongelmia mielessä näin kehystämällä se on yhteensopivampi kielten kuten C kanssa. Lupaukset ovat todennäköisesti peräisin IO:n valmistumisen mallista.

Täytäntöönpanon puute verrattuna tehdasmalliin

Eräs panostaja ajattelee, että tämä malli "on huono idea, koska ilman tehtaan funktiota ei ole mitään, mikä pakottaisi valmiuden tarkistamisen invariantin. Se jää asiakkaiden vastuulle, ja voit käytännössä olla varma, että he mokaillevat sitä silloin tällöin."

Jos otat tämän kannan, miten aiot estää ihmisiä luomasta tehtaan menetelmiä, jotka eivät pakota tarkistusta? Missä vedät rajan? Esimerkiksi, kieltäisitkö jakolaskuoperaattorin, koska mikään ei estä ihmisiä antamasta nollalla jakajaa? Kova totuus on, että sinun täytyy oppia erottaamaan toimialakohtainen koodi ja kehyskohtainen koodi ja soveltaa erilaisia standardeja, höystettynä hieman maalaisjärkeä.


Edeltäjät

Tämä on alkuperäinen työni. Kehittelin tämän suunnittelumallin, koska olin tyytymätön ulkopuolisiin tehtaisiin ja muihin vastaaviin kiertoteihin. Vaikka etsin jonkin aikaa, en löytänyt aiempaa työtä ratkaisulleni, joten väitän olevani tämän mallin alkuperäinen luoja, ellei toisin todisteta.

Siitä huolimatta havaitsin vuonna 2020, että vuonna 2013 Stephen Cleary julkaisi erittäin samanlaisen ratkaisun ongelmaan. Katsellessani omaa työtäni taaksepäin tämän lähestymistavan ensimmäiset jäljet ilmestyvät koodiin, jota työskentelin suunnilleen samaan aikaan. Epäilen, että Cleary yhdisti kaiken ensin, mutta hän ei muotoillut sitä suunnittelumalliksi tai julkaissut sitä siten, että muut ongelman kanssa työskentelevät löytäisivät sen helposti. Lisäksi Cleary käsittelee vain rakentamista, joka on vain yksi Readiness-mallin sovellutuksista (katso alla).

Yhteenveto

Kuvio on

  • laita lupaus esineeseen, jota se kuvaa
  • paljasta se ominaisuutena nimeltä Ready
  • viittaa aina lupaukseen Ready-ominaisuuden kautta (älä tallenna sitä asiakasohjelman koodimuuttujaan)

Tämä luo selkeän yksinkertaisen semantiikan ja takaa, että

  • lupaus luodaan ja hallitaan
  • lupauksella on sama laajuus kuin sen kuvaamalla objektilla
  • valmiuden riippuvuuden semantiikka on ilmeinen ja selvä asiakaskoodissa
  • jos lupaus korvataan (esim. yhteys menee valmiiksi ja sitten uudelleen valmiiksi verkkoyhteyden olosuhteiden vuoksi), asiakaskoodi, joka viittaa siihen thing.Ready-kautta, käyttää aina nykyistä lupausta

Tämä viimeinen on painajainen, kunnes käytät kaavaa ja annat objektin hallita omaa lupaustaan. Se on myös erittäin hyvä syy olla tallentamatta lupausta muuttujaan.

Jotkut objektit sisältävät metodeja, jotka asettavat ne väliaikaisesti epäkelpoiseen tilaan, ja malli voi toimia siinä tilanteessa ilman muutoksia. Koodi muodossa obj.Ready.then(...) käyttää aina mitä tahansa lupausominaisuutta, jonka Ready-ominaisuus palauttaa, joten aina kun jokin toiminto on lähdössä tekemään objektin tilasta kelvotonta, voidaan luoda uusi lupaus.

Lopetussanat

Valmiuskuvio ei ole erityinen rakennusalalle. Sitä on helppo soveltaa rakentamiseen, mutta se koskee todellisuudessa sitä, että varmistetaan tilariippuvuudet. Nykyisin, kun käytetään asynkronista koodia, tarvitset järjestelmän, ja lupauksen yksinkertaiset deklaratiiviset semantiikat tekevät sen ilmaisemisesta suoraviivaista, että toiminta tulisi suorittaa mahdollisimman pian, painottaen mahdollista. Kun alat hahmottaa asioita näissä termeissä, pitkien metodien tai konstruktoreiden ympärillä käytävät keskustelut menettävät merkityksensä.

Viivästetty alustaminen on silti paikoillaan; kuten mainitsin, voit yhdistää valmiuden laiskaan lataukseen. Mutta jos on todennäköistä, ettet käytä objektia, miksi luoda se aikaisin? Saattaa olla parempi luoda se tarpeen mukaan. Tai ei ehkä ole; joskus et voi sietää viivettä tarpeen tunnistamisen ja sen täyttämisen välillä.

On olemassa enemmän kuin yksi tapa nylkeä kissa. Kun kirjoitan sulautettua ohjelmistoa, luon kaiken etukäteen mukaan lukien resurssipoolit. Tämä tekee vuodoista mahdottomia ja muistin tarpeet tiedetään käännösaikana. Mutta se on vain ratkaisu pieneen suljettuun ongelma-alueeseen.

Vastaus #3

Käytä sen sijaan asynkronista tehtaanmetodia.

class MyClass {
   private mMember: Something;

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

Tulee :

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;
   };
}

Tämä tarkoittaa, että sinun täytyy odottaa tämän tyyppisten objektien rakentamista, mutta se on jo oletettavasti ilmeistä siitä tosiasiasta, että olet tilanteessa, jossa sinun täytyy joka tapauksessa odottaa jotain rakentaaksesi niitä.

On yksi asia, jonka voit tehdä, mutta epäilen, ettei se ole hyvä idea:

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

   constructor() {
      this.LoadAsync();
   }

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

Tämä voi toimia, enkä ole koskaan aiemmin kokenut siitä todellista ongelmaa, mutta minusta se tuntuu vaaralliselta, koska objektisi ei ole todellisuudessa täysin alustettu, kun alat käyttää sitä.

Toinen tapa tehdä se, joka saattaa joiltakin osin olla parempi kuin ensimmäinen vaihtoehto, on odottaa osia ja rakentaa sitten objektisi sen jälkeen:

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);
   };
}

Vastaus #4

Olen löytänyt ratkaisun, joka näyttää siltä

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
  }

Se toimii, koska Promiset, jotka tehostavat async/awaitia, voidaan ratkaista useita kertoja samalla arvolla.

Vastaus #5

Käytä yksityistä konstruktoria ja staattista tehdasmetodia FTW. Se on paras tapa pakottaa mikä tahansa validointilogiikka tai datan rikastaminen, kapseloituna asiakkaalta eristettynä.

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');

Vastaus #6

Tiedän, että se on melko vanha, mutta toinen vaihtoehto on käyttää tehdasta, joka luo objektin ja odottaa sen alustamista:

// 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

Aiheeseen liittyviä artikkeleita