Bu makalemizde, JavaScript'de Promises/A+ yöntemi ile asenkron yapıdan kaynaklı iç içe geçmiş ve karmakarışık olmuş kodların nasıl daha düzgün ve sade bir şekilde yazılabileceğini inceleyeceğiz.

Makaleye başlamadan önce şunu söylemeliyim ki, Promises yöntemi gerçekten de çok önemli ve JavaScript'i programlama dili olarak kullanan herkesin mutlaka öğrenmesi gereken bir konu. Öyle ki, bu yönteme bir kez alışan, artık bir daha klasik asenkron mantığında fonksiyon yazmak veya kullanmak istemeyecektir. Ancak buna subjektif ikna yoluyla değil de, gerçek kullanım şekilleriyle yaklaşalım ve kararı sana bırakalım.

Bildiğin gibi, özellikle Node.JS başta olmak üzere JavaScript'teki pek çok fonksiyonu asenkron şekilde yazıyoruz (veya yazmak zorunda kalıyoruz). Asenkron tek bir fonksiyonu çağırmak ve callback fonksiyonunu yazmak çok da zor olmuyor ancak arka arkaya birkaç asenkron fonksiyonu çağırmak zorunda kalırsak, aşağıdaki gibi bir görüntü ile karşılaşıyoruz.




var persister = new Persister();

persister.set('key1', 'value1',
    function (err) {
        if (err) {
            console.log(err);
            return;
        }

        persister.set('key2', 'value2',
            function (err) {
                if (err) {
                    console.log(err);
                    return;
                }

                persister.set('key3', 'value3',
                    function (err) {
                        if (err) {
                            console.log(err);
                            return;
                        }

                        persister.get('key1',
                            function (err, value) {
                                if (err) {
                                    console.log(err);
                                    return;
                                }

                                console.log(value);
                            });
                    });
            });
    });

Ecnebiler, ekranın sağından çıkacakmış gibi gözüken bu koda Pyramid of Doom diyorlar. Promises yöntemini kullandığımız taktirde, aynı kodu aşağıdaki gibi yazmamız mümkün oluyor.

var persister = new Persister();

persister.set('key1', 'value1')
    .then(function () {
        return persister.set('key2', 'value2');
    })
    .then(function () {
        return persister.set('key3', 'value3');
    })
    .then(function () {
        return persister.get('key1');
    })
    .then(function (value) {
        console.log(value);
    })
    .catch(function (err) {
        console.log(err);
    });

Şimdi, eğer ki "Ben ilk kod örneği ile de yaşayabilirim. Hem de daha çok iş yapmış gözükürüm" şeklinde düşünüyorsan, günün geri kalanında çıkıp sevdiklerinle vakit geçirebilirsin. Aksi görüşte olanlarla, temelden başlayarak adım adım devam edelim.

Sanırım hepimiz kabul ederiz ki, senkron çalışan kod yazmak ve kullanmak, asenkron çalışan kod yazmaya ve kullanmaya göre çok daha kolaydır. Promises yönteminin hedefi de, asenkron fonksiyonların avantajlarından faydalanırken, senkron fonksiyonların rahatlığından da ödün vermemek olarak özetlenebilir. Peki ama nasıl?

Çok detaya girerek kafa karıştırmayacağım ama temel düzeyde bilgi sahibi olmamızda fayda var. Klasik yaklaşımda, asenkron fonksiyonlar parametre olarak bir callback fonksiyonu alırlar ve işlemi tamamladıklarında da, işlem sonucu ile birlikte bu callback fonksiyonunu çağırırlar. Bunun bir örneğini aşağıda görebiliriz.

function asyncFunction(callback) {
    setTimeout(function () {
        callback(null, "Ali");
    }, 0);
}

// Asenkron fonksiyonu kullanıyoruz
asyncFunction(function (error, result) {
    if (error) {
        console.log(error);
        return;
    }
    console.log(result);
});


Promises yöntemi ise konuya farklı bir boyuttan yaklaşır. Bu yöntemle yazılan fonksiyonlar, herhangi bir callback parametresi almazlar. Bunun yerine fonksiyondan senkron olarak, işlem tamamlandığında devreye girecek bir nesne dönerler. Fonksiyon içerisindeki işlemimiz tamamlandığında, dönülen nesnenin "then" fonksiyonu, hata alındığı durumda da "catch" fonksiyonu çalışır. Bunun bir örneğini aşağıda görebiliriz (Kafa karıştırmamak adına şimdilik "asyncFunction" isimli fonksiyonun içeriğini vermiyorum).

var response = asyncFunction();

response
    .then(function (result) {
        console.log(result);
    })
    .catch(function (error) {
        console.log(error);
    });

Üstelik bu yöntemle, (ilk baştaki Persister örneğindeki gibi) birden fazla fonksiyon arka arkaya zincirleme bağlanabilir. Böylece iç içe geçmiş kod kümeleri yerine, senkron gibi alt alta yazılmış bir koda sahip oluruz.

Promises dönen fonksiyonları kullanmak gerçekten de yukarıdaki kadar kolay. Gelelim bir fonksiyondan Promise nesnesini nasıl döneceğimize. Elbette Amerika'yı baştan keşfetmek her zaman mümkündür, bu tarz bir yapıyı kendimiz de kurabiliriz. Ancak JavaScript dünyasında bu iş için yaygın olarak kullanılan bazı kütüphaneler var. Bunlardan en popüleri hiç şüphesiz Q. Ancak 2013 sonbaharından itibaren Q'nun tahtını, özellikle performans konusunda sarsan bir rakibi çıktı. İsmi bluebird. Son zamanlarda yurt dışındaki forumlarda yoğun olarak, "Projemi Q'dan bluebird'e nasıl geçiririm?" şeklinde sorularla karşılaşıyorum. Bu yüzden makaleye bluebird ile devam edeceğim. Ancak kullanım açısından iki kütüphane arasında çok bir fark bulunmuyor.

Not : Angular ile yazılım geliştirenlerin, Promises için bluebird yerine Q'nun Angular'a dahil edilmiş ve özelleştirilmiş bir versiyonu olan $q servisini kullanmaları gerekir. Eğer bu servisi kullanıyorsan, makale boyunca gördüğün örneklerdeki "deferred.fulfill" ifadelerini "deferred.resolve" şeklinde değiştirmen durumunda, örnekler $q üzerinde de aynı şekilde çalışmaya devam edecektir.

Yine örnekler üzerinden adım adım gidelim. İlk olarak aşağıdaki kodlarla bir html sayfası hazırlayalım.

<!DOCTYPE html>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1254" />
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script src="bluebird-2.1.2.js"></script>
<script src="program.js"></script>
</head>
<body>
</body>
</html>

Burada "bluebird-2.1.2.js" isimli bir kütüphaneyi sayfamıza ekledik. Bu kütüphaneyi Github'dan indirebilirsin. İlgili JS dosyası, ben bu makaleyi yazarken şu adreste bulunuyordu.

Ardından aşağıdaki kodlarla program.js dosyamızı hazırlayalım.

function asyncFunction() {

    var deferred = Promise.pending();

    setTimeout(function () {
        deferred.fulfill("Ali");
    }, 0);

    return deferred.promise;
}

// Asenkron fonksiyonu kullanıyoruz
asyncFunction()
    .then(function (result) {
        $("body").append(result);
    });

Bu örneğin hazır yazılmışını çalıştırmak için buraya tıklayabilirsin. Karşına aşağıdaki gibi bir sayfanın gelmesi gerekiyor.



Burada "asyncFunction" isminde, asenkron bir fonksiyonumuz bulunuyor. Fonksiyonun ilk satırında, "deffered" isimli bir nesne tanımlıyoruz. Bu nesne, fonksiyonumuzdan Promise dönmemize imkan sağlayacak.

Hemen ardından "setTimeout" ile 0 milisaniye sonra çalışacak bir fonksiyon tanımlıyoruz. Bu yöntem literatürde "Zero Timeout Pattern" olarak geçer ve asenkron fonksiyon yazmak için sıkça kullanılır. Buna göre, "setTimeout" içerisinde yer alan fonksiyon, Event Loop'un bir sonraki dönüşünde işletilecektir. Bu sebeple, bu fonksiyon çalıştırılmadan önce fonksiyon sonundaki "return" ifadesi çalıştırılır ve Promise nesnemiz fonksiyondan senkron olarak geri dönülür.

Geldiğimiz nokta itibariyle "asyncFunction" fonksiyonumuzu çağırmış bulunuyor ve geriye cevap olarak senkron şekilde, şimdilik bir anlam ifade etmeyen bir nesne almış oluyoruz.

Bu noktada "setTimeout" içerisinde tanımlı olan fonksiyonun çalıştırılmasına sıra gelecektir. Bu fonksiyon içerisinde "deffered" nesnesinin "fulfill" fonksiyonunu çağırıyor ve parametre olarak da "Ali" geçiyoruz. Bu işlem, "asyncFunction" fonksiyonundan senkron olarak dönülen Promise nesnesinin "then" fonksiyonunun çağırılmasına ve parametre olarak da "Ali" geçilmesine sebep olacaktır. Biz de bunu, fonksiyonu çağırdığımız yerdeki "then" fonksiyonu içerisinde yakalıyor ve ekrana yazdırıyoruz.

Özetlersek, asenkron fonksiyonumuz, işlem tamamlandığında "then" fonksiyonu çağırılacak bir nesneyi senkron olarak döndü. Biz de fonksiyona bir callback parametresi geçmek yerine, bu "then" fonksiyonu içerisinde işlem sonucunu aldık ve gerekli işlemleri yerine getirdik.

Bu örneğimizde "deffered.fulfill" ifadesinin "then" fonksiyonunu tetiklediğini öğrenmiş olduk. Bir de işin olumsuz boyutu var. Bazı durumlarda, callback fonksiyonunu çağırmak yerine hata fırlatmayı isteriz. Bu durumu incelemek için "program.js" dosyamızın içeriğini aşağıdaki gibi güncelleyelim.

function asyncFunction() {

    var deferred = Promise.pending();

    setTimeout(function () {
        deferred.reject(new Error("Mesaj"));
    }, 0);

    return deferred.promise;
}

// Asenkron fonksiyonu kullanıyoruz
asyncFunction()
    .then(function (result) {
        $("body").append(result);
    })
    .catch(function (error) {
        $("body").append("Hata : " + error.message);
    });

Bu örneğin hazır yazılmışını çalıştırmak için buraya tıklayabilirsin. Karşına aşağıdaki gibi bir sayfanın gelmesi gerekiyor.


Bu örneğimiz de, bir önceki örneğimize çok benziyor. Fark olarak "setTimeout" içerisinde "deffered.fulfill" yerine "deffered.reject" şeklinde bir çağrım yapıyoruz ve parametre olarak da bir Error nesnesi geçiyoruz. Buradaki "reject" ifadesi, fonksiyonun çağırıldığı yerdeki "catch" fonksiyonunun tetiklenmesine sebep olur. Nitekim öyle de oluyor ve hata mesajı ekrana yazdırılıyor.

Böylece ihtiyaç durumuna göre "deffered.fulfill" ve "deffered.reject" ifadeleri ile asenkron fonksiyondan olumlu ve olumsuz nasıl dönüş yapacağımızı görmüş olduk. Ancak bu tarz tek bir fonksiyon kullanımı ile Promises yönteminin faydasını görmek çok mümkün değil. Bunu en iyi, zincirleme asenkron fonksiyon çağırımında görebiliriz. "program.js" dosyamızın içeriğini aşağıdaki gibi güncelleyelim.

function asyncFunction1(name) {

    var deferred = Promise.pending();

    setTimeout(function () {
        deferred.fulfill("Selam 1, " + name + "<br>");
    }, 0);

    return deferred.promise;
}

function asyncFunction2(name) {

    var deferred = Promise.pending();

    setTimeout(function () {
        deferred.fulfill("Selam 2, " + name + "<br>");
    }, 0);

    return deferred.promise;
}

function asyncFunction3(name) {

    var deferred = Promise.pending();

    setTimeout(function () {
        deferred.fulfill("Selam 3, " + name + "<br>");
    }, 0);

    return deferred.promise;
}

// asyncFunction1 fonksiyonunu çağırıyoruz
asyncFunction1("Ali")
    .then(function (result) {

        // asyncFunction1 fonksiyonunun cevabını ekrana yazıyoruz
        $("body").append(result);

        // asyncFunction2 fonksiyonunu çağırıyoruz
        return asyncFunction2("Veli");
    })
    .then(function (result) {

        // asyncFunction2 fonksiyonunun cevabını ekrana yazıyoruz
        $("body").append(result);

        // asyncFunction3 fonksiyonunu çağırıyoruz
        return asyncFunction3("Ayse");
    })
    .then(function (result) {

        // asyncFunction3 fonksiyonunun cevabını ekrana yazıyoruz
        $("body").append(result);
    })
    .catch(function (error) {
        $("body").append("Hata : " + error.message);
    });

Bu örneğin hazır yazılmışını çalıştırmak için buraya tıklayabilirsin. Karşına aşağıdaki gibi bir sayfanın gelmesi gerekiyor.


Burada üç adet Promise dönen asenkron fonksiyonumuz var. İlk fonksiyonu çağırmada yeni bir bilgi yok. Ancak dikkat edersen, ikinci fonksiyonu çağırma sırasında "return" ifadesini kullanıyoruz. Böylece bu fonksiyondan dönen Promise nesnesi de, hemen arkasından gelen "then" ifadesi tarafından yakalanıyor. Bu şekilde zincirleme bir kullanım elde etmiş oluyoruz ki, bizi Pyramid of Doom'dan kurtaracak yöntem de bu oluyor. Ayrıca tüm blok için tek bir "catch" ifadesinin bulunduğuna da dikkatini çekerim. Bu "catch" ifadesi, tüm çağrımlar için ortak olarak kullanılacaktır.


Sık Yapılan Yanlışlar

Buraya kadar anlatılanlarla Promises yöntemi ile kod yazmaya başlamamız mümkün. Ancak çok sık karşılaştığım iki hatalı kullanım şeklini göstermek ve önceden uyarmak istiyorum. "program.js" dosyamızın içeriğini aşağıdaki gibi güncelleyelim.

function asyncFunction1(name) {

    var deferred = Promise.pending();

    setTimeout(function () {
        deferred.fulfill("Selam " + name);
    }, 0);

    return deferred.promise;
}

function asyncFunction2(name) {

    var deferred = Promise.pending();

    // asyncFunction1 fonksiyonu çağırımı
    asyncFunction1(name)
        .then(function (result) {
            deferred.fulfill(result);
        })
        .catch(function (error) {
            deferred.reject(error);
        });

    return deferred.promise;
}


// asyncFunction2 fonksiyonu çağırımı
asyncFunction2("Ali")
    .then(function (result) {
        $("body").append(result);
    })
    .catch(function (error) {
        $("body").append("Hata : " + error.message);
    });

Bu örneğin hazır yazılmışını çalıştırmak için buraya tıklayabilirsin. Karşına aşağıdaki gibi bir sayfanın gelmesi gerekiyor.


Burada zincirleme değil, iç içe geçmiş iki adet Promise dönen asenkron fonksiyon görüyoruz. "asyncFunction1", bizim temel asenkron fonksiyonumuz. Diyelim ki "asyncFunction2" içerisinde bu fonksiyonun çağırılması ve duruma göre dönüş yapılması gerekiyor. Bunun için yukarıdaki örnekte, "asyncFunction2" fonksiyonu içerisinde şimdiye kadar öğrendiğimiz yöntemler ile Promise nesnesi yakalanıp, duruma göre dahili Promise nesnesinin "fulfill" veya "reject" fonksiyonlarının çağırılması sağlanıyor.

Tamam, sonuç olarak yazılan bu kod çalışıyor. Ancak zaten Promise dönen bir fonksiyon için bu şekilde bir çağırım yapmak ve ikinci bir Promise nesnesi oluşturmak gereksiz. Bunun yerine dönülen Promise nesnesini direk dönebiliriz. Yani doğru kullanım şekli aşağıdaki gibi olmalı.

function asyncFunction1(name) {

    var deferred = Promise.pending();

    setTimeout(function () {
        deferred.fulfill("Selam " + name);
    }, 0);

    return deferred.promise;
}

function asyncFunction2(name) {
    // asyncFunction1 fonksiyonu çağırımı
    return asyncFunction1(name);
}

// asyncFunction2 fonksiyonu çağırımı
asyncFunction2("Ali")
    .then(function (result) {
        $("body").append(result);
    })
    .catch(function (error) {
        $("body").append("Hata : " + error.message);
    });

Bu örneğin hazır yazılmışını çalıştırmak için buraya tıklayabilirsin. Karşına aşağıdaki gibi bir sayfanın gelmesi gerekiyor.



Gelelim diğer bir yanlış kullanım örneğine. Asenkron callback fonksiyonlarda "(error, result)" şeklindeki kullanımlar oldukça yaygındır. Bu kullanıma göre, callback fonksiyonuna geçilen ilk parametre işlem sonucunda oluşan hatayı gösterir ve hata olmaması durumunda null değer alır. İkinci parametre ise işlemin başarılı olduğu durumdaki gerçek işlem sonucudur. Aşağıda "asyncFunction1" ismindeki bu tarz bir fonksiyonu Promises yöntemine çeviren "asyncFunction2" isimli bir çevreleyici fonksiyon örneğini görüyoruz.

function asyncFunction1(name, callback) {

    setTimeout(function () {
        callback(null, "Selam " + name);
    }, 0);
}

function asyncFunction2(name) {

    var deferred = Promise.pending();

    // Klasik callback fonksiyon çağırımı
    asyncFunction1(name, function(error, result){
        if(error){
            deferred.reject(error);
        }
        else {
            deferred.fulfill(result);
        }
    });

    return deferred.promise;
}

// Promises yöntemi ile fonksiyon çağırımı
asyncFunction2("Ali")
    .then(function (result) {
        $("body").append(result);
    })
    .catch(function (error) {
        $("body").append("Hata : " + error.message);
    });

Bu örneğin hazır yazılmışını çalıştırmak için buraya tıklayabilirsin. Karşına aşağıdaki gibi bir sayfanın gelmesi gerekiyor.


Dikkat : Bu örnek neden önemli? Çünkü JavaScript kütüphanelerinde bu tarz bir callback üzere çalışan hali hazırda pek çok fonksiyon bulunuyor ve biz bunları Promises yöntemine çevirmek için buradaki gibi Adapter fonksiyonlarını yazmamız gerekiyor.

Evet, yukarıdaki örnek mantıklı ve çalışıyor olabilir ancak kütüphaneleri geliştiren amcalar, bu işin çok sık yapıldığını fark etmişler ve bizi tekrar tekrar Adapter fonksiyon yazmaktan kurtarmak için hazır bir fonksiyon sunmuşlar. "promisify" ismindeki bu fonksiyon sayesinde, yukarıda bahsedilen şekildeki fonksiyonları tek satırda Promises yöntemine çevirebiliyoruz. Bu yüzden doğru kullanım şekli aşağıdaki gibi olmalı.

function asyncFunction1(name, callback) {

    setTimeout(function () {
        callback(null, "Selam " + name);
    }, 0);
}

// callback -> Promises dönüşümü
var asyncFunction2 = Promise.promisify(asyncFunction1);


// Promises yöntemi ile fonksiyon çağırımı
asyncFunction2("Ali")
    .then(function (result) {
        $("body").append(result);
    })
    .catch(function (error) {
        $("body").append("Hata : " + error.message);
    });

Bu örneğin hazır yazılmışını çalıştırmak için buraya tıklayabilirsin. Karşına aşağıdaki gibi bir sayfanın gelmesi gerekiyor.


Görüldüğü üzere "promisify" fonksiyonu işimizi oldukça kolaylaştırıyor.


Geldik bir makalenin daha sonuna. Öğrendiklerimizi özetlemeye çalışalım.
  • Promises yöntemi ile, asenkron yapıdan kaynaklı iç içe geçmiş ve karmakarışık olmuş kodları daha düzgün ve sade bir şekilde yazabiliriz.
  • Promises yöntemini kullanmak için çeşitli kütüphaneler kullanımımıza sunulmuştur. An itibariyle bunların en popülerleri Q ve bluebird isimli kütüphanelerdir.
  • Promises yöntemi ile yazılan fonksiyonlar, herhangi bir callback parametresi almazlar. Bunun yerine fonksiyondan senkron olarak, işlem tamamlandığında devreye girecek bir nesne dönerler.
  • Asenkron fonksiyon içerisindeki Promise nesnesinin "fulfill" fonksiyonu çağırıldığı taktirde, fonksiyondan senkron olarak dönülen Promise nesnesinin "then" fonksiyonu tetiklenir.
  • Benzer şekilde fonksiyon içerisindeki Promise nesnesinin "reject" fonksiyonu çağrıldığı taktirde, fonksiyondan senkron olarak dönülen Promise nesnesinin "catch" fonksiyonu tetiklenir.
  • Birden fazla asenkron fonksiyon, zincirleme şekilde arka arkaya çağrılarak "Pyramid of Doom" oluşumundan kaçınılabilir.
  • "promisify" fonksiyonu sayesinde, klasik callback mantığındaki asenkron fonksiyonlar kolaylıkla Promises yöntemine geçirilebilir.


Geri
Underscore
İleri
Bu en son makaledir

Yorum Gönder

  1. Güzel bir konu olmuş. Teşekkür ederim.

    YanıtlaSil
  2. bu durum performans açısından etkisi dezavantajları var mıdır? nodejs de bu şekilde kullanımın tek thread yapısı olumsuz etkiler mi bilgi verebilirseniz faydalı olabilir. Teşekkürler

    YanıtlaSil

 
Top