Pemrograman Asinkron

Pada bagian sebelumnya, kita telah melihat dua model pemrograman asinkron, yaitu AJAX dan WebSocket. Kedua model pemrograman ini memiliki pola pemrograman yang sama, yaitu:

  1. Kirimkan data atau perintah ke server secara asinkron.
  2. Buat kode yang akan menangani respon dari server.
  3. Lanjutkan kode aplikasi.
  4. Ketika server telah mengembalikan data, jalankan kode pada no 2.

Model pemrograman yang menunggu sesuatu terjadi untuk kemudian menjalankan kode tertentu seperti ini kita kenal dengan nama Event-Based Programming (EBP). Meskipun dapat bekerja dengan sangat baik, model EBP seperti ini cukup kompleks dan dapat menyebabkan kode program menjadi sulit untuk dirawat. Misalkan kode seperti berikut:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    $.ajax("/feature/", function(html) {
            // (#1) melakukan sesuatu

            $.ajax("/feature/subfeature", function(css) {
                    // (#2) melakukan sesuatu yang lebih kompleks

                    $.ajax("/feature/subfeature2", function() {
                            // (#3) melakukan sesuatu lagi
                    });

                    // (#4) lanjut eksekusi #2
            });

            // (#5) lanjut eksekusi #1
    });

Pada format pemrograman di atas yang mengandalkan fungsi anonim sebagai callback terhadap sebuah event yang terjadi menyebabkan perawatan program lebih kompleks dari seharusnya. Jika misalnya terdapat kesalahan pada bagian #3, kita akan kesulitan mendapatkan informasi lengkap (biasanya *stack trace*) dari kode di atas. Kesulitan ini disebabkan berlapisnya fungsi anonim yang digunakan, dan terkadang pesan kesalahan atau Exception yang dilemparkan oleh fungsi anonim di dalam belum tentu dapat dibaca oleh fungsi luarnya.

Selain masalah di atas, umumnya juga sering terjadi kesulitan dalam mengikuti alur eksekusi kode program seperti di atas. Contohnya, ketika #2 berjalan, fungsi yang menampung #3 dapat saja dijalankan sebelum atau sesudah #4, tergantung dari banyak faktor. Hal yang sama juga ditemui untuk eksekusi #2 dan #5 (otomatis diikuti oleh eksekusi #3 serta #4). Masalah lain lagi yaitu ketika kode di bagian #4 bergantung pada hasil yang ada pada #3. Kita harus menggunakan mekanisme event yang lebih kompleks lagi, yaitu callback, untuk memastikan #4 berjalan setelah #3.

Permsalahan-permasalahan yang dikemukakan di atas kemudian melahirkan konstruk pemrograman baru [1], yang diharapkan dapat membantu kita dalam menulis program asinkron.

Promise

Promise merupakan sebuah objek yang merepresentasikan sebuah nilai yang belum ada sekarang, tetapi akan ada pada satu titik di masa depan. Contohnya, kita dapat menggunakan Promise untuk mengambil data dari lokasi luar (misal: data tweet pada Twitter). Ketika Promise dibuat, nilai dari data yang belum datang tidak mungkin diketahui. Untuk menanggulangi hal ini, Promise akan bertindak sebagai penengah sementara sampai data selesai diambil. Sementara Promise sedang bekerja di balik layar, kita dapat memperlakukan objek Promise layaknya nilai kelas pertama. Hal ini berarti kita dapat menyimpannya ke dalam variabel, mengirimkan objek ke fungsi, mengembalikan objek dari fungsi, dan apapun yang dapat dilakukan oleh variabel lainnya.

Kemampuan untuk memperlakukan operasi asinkron layaknya sebuah nilai kelas pertama ini akan sangat memudahkan kita dalam melakukan pemrograman asinkron. Sederhananya, kita dapat menulis kode asinkron yang seolah-olah adalah kode sinkron. Hal ini tentunya akan sangat menyederhanakan kode program kita, yang pada akhirnya berimbas pada kemudahan perawatan kode.

Pembuatan Objek Promise

Membuat objek Promise sendiri dapat dilakukan dengan sangat sederhana:

1
2
3
4
5
6
7
8
9
var promise = new Promise(function (resolve, reject) {
    // Kode asinkron (AJAX, WebSocket, dll)

    if (/* semuanya lancar tanpa error */) {
            resolve(data);
    } else {
            reject(Error("Terjadi kesalahan."));
    }
});

Kita hanya cukup memanggil constructor dari objek promise, yang menerima satu fungsi. Fungsi yang diberikan kepada Promise dapat diisikan dengan dua parameter, yaitu:

  1. Parameter pertama yang adalah sebuah fungsi yang dijalankan jika kode asinkron telah selesai berjalan, dan berjalan tanpa masalah.
  2. Parameter kedua sifatnya opsional, berisi fungsi yang dijalankan ketika terjadi kesalahan dalam eksekusi kode asinkron.

Isi dari badan fungsi yang dikirimkan ke Promise sendiri tidak dibatasi. Misalnya, kita dapat mengisikan fungsi tersebut dengan pemanggilan AJAX seperti berikut:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
var promise = new Promise(function (resolve, reject) {
    var xhr;

    try {
            xhr = new XMLHttpRequest();
    } catch (e) { reject(Error("XHR tidak didukung browser.")); }

    xhr.onreadystatechange = function () {
            if (xhr.readyState === 4 && xhr.status === 200) {
                    resolve(xhr.responseText);
            }
    };

    xhr.open('GET', 'http://example.com/testajax/');
    xhr.send();

    });

Seperti yang dilihat dari kode di atas, kita pada dasarnya melakukan pemanggilan AJAX di dalam fungsi yang dikirimkan ke Promise. Perbedaan dari kode yang kita tulis di atas adalah pemanggilan fungsi resolve dan reject ketika kode AJAX berhasil ataupun gagal berjalan.

Sebuah Promise yang telah kita buat dapat memiliki tiga status keadaan:

  1. Pending, yaitu jika nilai hasil eksekusi asinkron Promise belum dapat diakses.
  2. Fulfilled, yaitu ketika nilai sudah dapat diakses. Nilai ini nantinya akan menjadi nilai permanen dari Promise.
  3. Rejected, yaitu nilai yang didapatkan kalau eksekusi asinkron gagal berjalan. Sama seperti fulfilled, nilai dari rejected akan diasosiasikan kepada Promise secara permanen. Nilai rejected biasanya berupa objek Error, meskipun tidak terdapat batasan khusus apa yang harus kita isikan di sini.

Ketika Promise dibuat, secara otomatis Promise akan berada pada status pending. Kita kemudian dapat menggunakan Promise selayaknya variabel biasa. Utilisasi dari hasil eksekusi asinkron Promise (setelah status berubah menjadi fulfilled atau rejected) sendiri harus dilakukan melalui pemanggilan method khusus.

Mengambil Hasil Eksekusi Promise

Meskipun Promise dapat digunakan layaknya variabel sembari menjalankan eksekusi asinkron di balik layar, sampai satu titik tertentu kita tetap akan harus mendapatkan hasil eksekusi tersebut. Ketika ingin mengakses nilai hasil eksekusi asinkron dari Promise kita dapat memanggil method then, seperti berikut:

1
2
3
4
5
6
7
8
promise.then(
        function (data) {
            // jika promise sukses dijalankan
        },
        function (error) {
            // jika promise gagal dijalankan
        }
    );

Method then menerima dua argumen: sebuah fungsi yang dipanggil ketika Promise selesai dijalankan (fulfilled), dan sebuah fungsi yang dijalankan ketika Promise gagal dijalankan (rejected). Kedua parameter ini sifatnya opsional, sehingga kita dapat menambahkan hanya salah satu dari fungsi tersebut.

Beberapa hal yang perlu diingat mengenai akses hasil eksekusi dan method then:

  1. Sebuah Promise hanya bisa berhasil atau gagal dieksekusi satu kali saja. Sebuah Promise yang sudah berhasil atau gagal dijalankan juga tidak dapat berubah status (misal: awalnya gagal, kemudian menjadi berhasil). Hal ini berarti hasil eksekusi Promise akan selalu konsisten begitu selesai dijalankan.
  2. Jika Promise telah selesai dijalankan (tidak perduli berhasil atau gagal), dan kita memasukkan fungsi baru untuk dijalankan jika berhasil, Promise akan memanggil kembali fungsi tersebut dengan data terakhir.

Kedua hal di atas sangat membantu kita dalam melakukan pemrograman asinkron, karena kita tidak lagi harus peduli kapan sesuatu dapat diakses, melainkan apa yang harus dilakukan ketika hal tersebut dapat diakses.

Promise Berantai

Seringkali ketika kita melakukan operasi asinkron, kita ingin melanjutkan satu operasi asinkron dengan operasi asinkron lainnya. Misalnya, katakanlah kita ingin mengambil data timeline dari Twitter beberapa pengguna, dan kemudian menyusun data tersebut sesuai tanggal penulisan. Ketika pengambilan data dari beberapa pengguna, tentunya kita harus melakukan beberapa pemanggilan asinkron secara berkesinambungan dan memastikan seluruh pemanggilan tersebut telah selesai sebelum mulai menyusunnya.

Kita dapat menggunakan dua cara untuk melakukan pemanggilan Promise secara berantai seperti yang dijelaskan sebelumnya, yaitu:

  1. Mengembalikan objek Promise pada fungsi yang dikirimkan ke then, dan kemudian memanggil then dari fungsi tersebut lagi.
  2. Menggunakan method Promise.all yang dirancang untuk memanggil beberapa Promise sekaligus.

Mari kita langsung lihat metode yang pertama terlebih dahulu. Asumsikan kita memiliki sebuah fungsi yang mengembalikan Promise seperti berikut:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// MyLib.AJAX merupakan fungsi untuk pemanggilan AJAX
// yang dibahas pada bab sebelumnya.
var get = function (url, success, fail) {
    MyLib.AJAX({
        onSuccess: success,
        onFailed: fail,
        url: url,
        method: "GET"
    });
},
getPromised = function (url) {
    return new Promise(function (resolve, reject) {
        get(url, resolve, reject);
    });
};

Kita kemudian dapat menggunakan getPromised seperti berikut:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var promise = getPromised("https://example.com/api/testing");

promise.then(function (data) {
    // lakukan sesuatu
});

// atau langsung:
getPromised("https://example.com/api/testing").then(function (data) {
    // lakukan sesuatu
});

Ketika kita mengembalikan nilai dari fungsi yang diberikan pada then, kita dapat menyambung pemanggilan then untuk melakukan operasi lebih lanjut kepada nilai tersebut:

1
2
3
4
5
6
7
8
getPromised("https://example.com/api/testing").then(function (data) {
    // lakukan sesuatu

    console.log(data); // asumsi: data = 10
    return data + 1;
}).then(function (val) {
    console.log(val); // 11 (data + 1)
});

Dengan prinsip yang sama, ketika kita mengembalikan objek Promise di dalam fungsi then, maka operasi Promise pada fungsi kedua hanya dapat dijalankan setelah operasi pertama selesai. Perhatikan kode berikut:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
getPromised("https://example.com/api/testing")
        .then(function (data) {
            // lakukan sesuatu

            // ...

            return getPromised("https://example.com/api/percobaan");
        })
        .then(function (data) {
            // fungsi ini hanya berjalan SETELAH
            // then pertama selesai.
        });

Karena then yang pertama baru akan dijalankan setelah pemanggilan ke https://example.com/api/testing selesai, maka dengan mengembalikan sebuah Promise baru kita akan memastikan then kedua hanya berjalan setelah Promise selesai berjalan.

Cara lain untuk menjalankan beberapa Promise sekaligus adalah dengan menggunakan method Promise.all. Method Promise.all menerima sebuah array dari Promise, yang akan dijalankan dan dikembalikan hasilnya kepada kita pada fungsi then:

1
2
3
4
5
6
7
8
var p1   = getPromised("https://example.com/api/testing");
var p2   = getPromised("https://example.com/api/percobaan");
var allP = [p1, p2];

Promise.all(allP).then(function (results) {
    // results adalah array berisi hasil dari
    // p1 dan p2, berurutan
});

Seperti yang dapat dilihat pada kode di atas, fungsi then pada Promise.all sedikit berbeda, karena memberikan kita akses kepada hasil dari eksekusi seluruh promise yang dikirimkan, secara berurutan di dalam sebuah array. Sedikit kekurangan dari Promise.all adalah jika terdapat satu saja Promise yang gagal dijalankan, maka semua Promise yang dikirimkan akan dianggap gagal.

Perlombaan Promise

Ketika mengembangkan aplikasi yang bergantung kepada data asinkron, terkadang kita hanya memerlukan salah satu sumber data saja, tetapi menggunakan beberapa sumber untuk mendapatkan data yang tercepat ataupun terbaik. Misalnya, kita sedang mengembangkan aplikasi pemetaan yang mengambil data dari Google Maps (GM) dan Open Street Map (OSM). Kita dapat saja ingin menampilkan peta secepatnya, tidak peduli apakah data peta berasal dari GM atau OSM. Setelah mendapatkan peta, tentunya kita tidak ingin mengambil peta yang satunya lagi, untuk menghemat bandwidth pengguna. Untuk dapat melakukan hal ini, kita dapat menggunakan method Promise.race.

Promise.race menjalankan beberapa Promise sekaligus, dan kemudian mengambil hasil Promise pertama yang selesai (atau gagal) dieksekusi. Objek Promise lainnya yang belum selesai dieksekusi akan dihentikan begitu kita mendapatkan hasil. Cara kerjanya cukup sederhana, yaitu dengan menjalankan semua Promise bersamaan dan mengambil yang pertama kali memberikan respon. Berikut adalah contoh penggunaannya:

1
2
3
4
5
6
var p1 = new Promise(function(resolve, reject) { setTimeout(resolve, 500, "one"); });
    var p2 = new Promise(function(resolve, reject) { setTimeout(resolve, 100, "two"); });

    Promise.race([p1, p2]).then(function(value) {
      // value === "two"
    });

Sama seperti Promise.all, kita mengirimkan sebuah array berisi beberapa Promise ke Promise.race. Fungsi then kemudian akan memberikan satu nilai saja, yaitu nilai yang terlebih dahulu didapatkan. Dalam contoh kode di atas, kita dapat melihat bahwa p2 pasti akan selesai dijalankan terlebih dahulu, sehingga nilai value dapat dipastikan adalah “two”. Begitu kita mendapatkan hasil dari p2, nilai dari p1 sudah menjadi tidak penting karena value akan diisikan oleh hasil dari p2.

Penggunaan nilai awal ini juga berlaku bahkan ketika Promise yang lebih akhir selesai gagal dijalankan:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    var p3 = new Promise(function(resolve, reject) { setTimeout(resolve, 100, "three"); });
    var p4 = new Promise(function(resolve, reject) { setTimeout(reject, 500, "four"); });

    Promise.race([p3, p4]).then(
            function(value) {
              // value === "three"
            },
            function(reason) {
              // tidak dieksekusi
            }
    );

Bahkan ketika salah Promise terlebih dahulu gagal, Promise lain yang mungkin memberikan hasil juga tidak lagi diperhitungkan hasilnya:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    var p5 = new Promise(function(resolve, reject) { setTimeout(resolve, 500, "five"); });
    var p6 = new Promise(function(resolve, reject) { setTimeout(reject, 100, "six"); });

    Promise.race([p5, p6]).then(
            function(value) {
              // tidak dieksekusi
            }, function(reason) {
              // reason === "six"
            }
    );

Berdasarkan prilaku dari Promise.race yang mengedepankan Promise tercepat inilah kita memberikan nama “Perlombaan Promise”.

Catatan Kaki

[1]Baru pada saat masih dibuat. Konstruk baru ini, Promise, dikembangkan pada tahun 1976 (Friedman, Daniel; David Wise (1976). “The Impact of Applicative Programming on Multiprocessing”. International Conference on Parallel Processing, pp. 263-272.)
comments powered by Disqus
Kembali ke bertzzie.com