fungsi konstruktor async di TypeScript?


Saya memiliki beberapa pengaturan yang ingin saya lakukan selama konstruktor, tetapi sepertinya itu tidak diperbolehkan

Yang berarti saya tidak bisa menggunakan:

Bagaimana lagi saya harus melakukan ini?

Saat ini saya memiliki sesuatu di luar seperti ini, tetapi ini tidak dijamin berjalan dalam urutan yang saya inginkan?

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

  await topic.setup();

Jawaban #1

Sebuah konstruktor harus mengembalikan instance dari kelas yang ia 'bangun'. Oleh karena itu, tidak mungkin untuk mengembalikan Promise<...> dan menunggu (await) itu.

Anda dapat:

  1. Buat pengaturan publik Anda async.

  2. Jangan memanggilnya dari konstruktor.

  3. Panggil itu kapan pun Anda ingin 'menyelesaikan' konstruksi objek.

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

Jawaban #2

Pola desain kesiapan

Telah diklaim bahwa

Sebuah konstruktor harus mengembalikan sebuah instance dari kelas yang dibangunnya. Oleh karena itu, tidak mungkin untuk mengembalikan Promise<...> dan menunggunya dengan await.

Ini tidak benar. Tidak ada persyaratan agar janji tersebut berada di akar grafik objek yang dikembalikan oleh konstruktor.

Jangan meletakkan objek dalam sebuah janji, letakkan sebuah janji dalam objek.

Kesiapan adalah sifat dari objek. Jadi jadikan itu sebagai sifat dari objek.

Metode initialise yang dapat ditunggu seperti yang dijelaskan dalam jawaban yang diterima memiliki keterbatasan serius. Menggunakan await langsung pada ekspresi konstruksi seperti ini berarti hanya jalur eksekusi yang dapat bergantung secara implisit pada kesiapan objek. Ini baik untuk kode dengan eksekusi linear yang dijamin tetapi dalam kode multi-threaded atau berbasis event hal ini tidak dapat diterima.

Anda bisa menangkap tugas/janji itu dan menunggu itu, tetapi bagaimana Anda mengelola agar ini tersedia untuk setiap konteks yang bergantung padanya?

Masalah menjadi lebih mudah diatasi ketika dirumuskan dengan benar. Tujuannya bukan menunggu konstruksi, tetapi menunggu kesiapan dari objek yang telah dibangun. Ini adalah dua hal yang benar-benar berbeda. Bahkan memungkinkan sesuatu seperti objek koneksi basis data berada dalam keadaan siap, kembali ke keadaan tidak siap, lalu menjadi siap lagi.

Bagaimana kita dapat menentukan kesiapan jika itu tergantung pada aktivitas yang mungkin belum selesai ketika konstruktor kembali? Jelas bahwa kesiapan adalah properti dari objek. Banyak kerangka kerja secara langsung mengekspresikan gagasan kesiapan. Dalam JavaScript kita memiliki Promise, dan dalam C# kita memiliki Task. Keduanya memiliki dukungan langsung dalam bahasa untuk properti objek.

Mengekspos janji penyelesaian konstruksi sebagai properti dari objek yang dibangun. Ketika bagian asinkron dari konstruksi Anda selesai, janji tersebut harus diselesaikan.

Tidak masalah apakah .then(...) dijalankan sebelum atau sesudah promise terpenuhi. Spesifikasi promise menyatakan bahwa memanggil then pada promise yang sudah terpenuhi hanya akan mengeksekusi handler segera.

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

Mengapa resolve(undefined); alih-alih resolve();? Karena ES6. Sesuaikan sesuai kebutuhan agar sesuai dengan target Anda.

Menggunakan await

Dalam sebuah komentar telah disarankan bahwa saya seharusnya membingkai solusi ini dengan await untuk lebih langsung menanggapi pertanyaan yang diajukan.

Anda dapat menggunakan await dengan properti Ready seperti yang ditunjukkan dalam contoh di atas. Saya bukan penggemar berat await karena itu mengharuskan Anda membagi kode Anda berdasarkan ketergantungan. Anda harus menempatkan semua kode yang tergantung setelah await dan semua kode yang tidak tergantung sebelumnya. Ini dapat menutupi maksud dari kode tersebut.

Saya mendorong orang untuk berpikir dalam istilah panggilan balik. Membingkai masalah secara mental seperti ini lebih kompatibel dengan bahasa seperti C. Janji bisa dibilang berasal dari pola yang digunakan untuk penyelesaian IO.

Kurangnya penegakan dibandingkan dengan pola pabrik

Seorang petaruh berpikir pola ini "adalah ide yang buruk karena tanpa fungsi pabrik, tidak ada yang menegakkan invariannya dalam memeriksa kesiapan. Itu diserahkan kepada klien, yang hampir bisa dipastikan akan membuat kesalahan dari waktu ke waktu."

Jika Anda mengambil posisi ini, lalu bagaimana Anda akan menghentikan orang dari membuat metode pabrik yang tidak menegakkan pemeriksaan? Di mana Anda menarik garisnya? Misalnya, apakah Anda akan melarang operator pembagian karena tidak ada yang mencegah orang menggunakan pembagi nol? Kenyataan pahitnya adalah Anda harus mempelajari perbedaan antara kode spesifik domain dan kode kerangka kerja serta menerapkan standar yang berbeda, dibumbui dengan sedikit akal sehat.


Asal-usul

Ini adalah karya asli dari saya. Saya merancang pola desain ini karena saya tidak puas dengan pabrik eksternal dan solusi sementara lainnya. Meskipun telah mencari untuk beberapa waktu, saya tidak menemukan karya sebelumnya untuk solusi saya, jadi saya mengklaim kredit sebagai pencipta pola ini sampai ada yang membantah.

Namun, pada tahun 2020 saya menemukan bahwa pada tahun 2013 Stephen Cleary memposting solusi yang sangat mirip untuk masalah tersebut. Melihat kembali pekerjaan saya sendiri, jejak awal pendekatan ini muncul dalam kode yang saya kerjakan sekitar waktu yang sama. Saya curiga Cleary menyatukannya terlebih dahulu tetapi dia tidak merumuskannya sebagai pola desain atau menerbitkannya di tempat yang mudah ditemukan oleh orang lain yang menghadapi masalah tersebut. Selain itu, Cleary hanya menangani konstruksi yang merupakan salah satu penerapan dari pola Readiness (lihat di bawah).

Ringkasan

Polanya adalah

  • letakkan sebuah janji di objek yang dijelaskannya
  • menyediakannya sebagai properti bernama Ready
  • selalu rujuk janji melalui properti Ready (jangan menangkapnya di variabel kode klien)

Ini menetapkan semantik yang jelas dan sederhana serta menjamin bahwa

  • janji itu akan dibuat dan dikelola
  • janji tersebut memiliki ruang lingkup yang identik dengan objek yang dijelaskannya
  • semantik ketergantungan kesiapan jelas dan nyata dalam kode klien
  • jika janji diganti (misalnya suatu koneksi menjadi tidak siap lalu siap kembali karena kondisi jaringan) kode klien yang merujuk padanya melalui thing.Ready akan selalu menggunakan janji yang sekarang

Yang terakhir ini adalah mimpi buruk sampai Anda menggunakan pola tersebut dan membiarkan objek mengelola janji (promise) itu sendiri. Ini juga merupakan alasan yang sangat baik untuk menahan diri dari menangkap janji ke dalam sebuah variabel.

Beberapa objek memiliki metode yang sementara menempatkan mereka dalam kondisi tidak valid, dan pola ini dapat digunakan dalam skenario itu tanpa modifikasi. Kode dalam bentuk obj.Ready.then(...) akan selalu menggunakan properti promise apa pun yang dikembalikan oleh properti Ready, jadi kapan pun suatu tindakan akan menonaktifkan status objek, sebuah promise baru dapat dibuat.

Catatan penutup

Pola Kesiapan tidak spesifik untuk konstruksi. Ini mudah diterapkan pada konstruksi tetapi sebenarnya tentang memastikan bahwa ketergantungan keadaan terpenuhi. Di zaman kode asinkron seperti sekarang, Anda memerlukan sebuah sistem, dan semantik deklaratif sederhana dari sebuah janji membuatnya mudah untuk menyatakan gagasan bahwa sebuah tindakan harus diambil secepat mungkin, dengan penekanan pada mungkin. Begitu Anda mulai membingkai hal-hal dalam istilah ini, argumen tentang metode atau konstruktor yang berjalan lama menjadi tidak relevan.

Inisialisasi tertunda masih memiliki tempatnya; seperti yang saya sebutkan, Anda dapat menggabungkan Kesiapan dengan pemuatan malas. Tapi jika kemungkinan besar Anda tidak akan menggunakan objek tersebut, lalu mengapa membuatnya lebih awal? Mungkin lebih baik membuatnya sesuai permintaan. Atau mungkin tidak; terkadang Anda tidak bisa mentolerir penundaan antara pengakuan kebutuhan dan pemenuhannya.

Ada lebih dari satu cara untuk menyelesaikan suatu masalah. Saat saya menulis perangkat lunak tertanam, saya membuat semuanya di awal termasuk kumpulan sumber daya. Ini membuat kebocoran menjadi tidak mungkin dan permintaan memori diketahui pada waktu kompilasi. Tetapi itu hanya solusi untuk ruang masalah kecil yang tertutup.

Jawaban #3

Gunakan metode pabrik asinkron sebagai gantinya.

class MyClass {
   private mMember: Something;

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

Menjadi :

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

Ini akan berarti bahwa Anda harus menunggu pembangunan jenis objek ini, tetapi itu seharusnya sudah tersirat dari fakta bahwa Anda berada dalam situasi di mana Anda harus menunggu sesuatu untuk membangunnya juga.

Ada hal lain yang bisa kamu lakukan tapi saya curiga itu bukan ide yang bagus:

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

   constructor() {
      this.LoadAsync();
   }

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

Ini bisa berhasil dan saya belum pernah mengalami masalah nyata darinya sebelumnya, tapi rasanya berbahaya bagi saya, karena objek Anda sebenarnya tidak akan sepenuhnya diinisialisasi saat Anda mulai menggunakannya.

Cara lain untuk melakukannya, yang mungkin lebih baik daripada opsi pertama dalam beberapa hal, adalah menunggu bagian-bagiannya, dan kemudian membangun objek Anda setelahnya:

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

Jawaban #4

Saya telah menemukan solusi yang terlihat seperti

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
  }

Ini bekerja karena Promise yang mendukung async/await, dapat diselesaikan beberapa kali dengan nilai yang sama.

Jawaban #5

Gunakan konstruktor pribadi dan metode pabrik statis FTW. Ini adalah cara terbaik untuk menerapkan logika validasi atau pengayaan data, yang dienkapsulasi terpisah dari klien.

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

Jawaban #6

Saya tahu ini cukup lama tetapi opsi lain adalah memiliki pabrik yang akan membuat objek dan menunggu inisialisasinya:

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

Artikel terkait