Node.js ile Async Callback Yönetimi

Node.js yazılarıma kullandığım bir şablon ile devam etmek istedim. Aslında bu problem bütün JavaScript genelinde geçerli fakat bu bölümde Node.js kullanırken veritabanı çağrılarında yaptığımız async işlemleri inceleyeceğiz. Öncelikle problemi tanımlayalım;

Önceki Node.js bir bakışta başlıklı yazımda Node.js ile yapılan sorguların async fakat javascript dilinin sync bir dil olduğundan bahsetmiştim. Bu durum aslında yönetmeyi biliyorsanız bir problem teşkil etmiyor fakat kodunuzun çok karışık gözükmesine sebep oluyor. Örneğin;

var productList = [];
dbHelper.getProducts(categoryId, function(data, err){
    productList = data;
});

for(var i = 0; i < productList.length; i++){
    console.log(productList[i].Name);
}

Yukarıdaki basit örnek çalışması düşük ihtimalli bir kod parçasına ait. dbHelper sınıfının getProducts metodunu bir değişken ile çağırıyoruz ve data cevabı bekliyoruz. Geri dönen cevabı productList değişkenine atıyoruz ve aşağıda productList içinde döngü kullanarak konsola ürün ismini yazdırmaya çalışıyoruz. Veritabanı detaylarını kodu basit tutmak için vermiyorum fakat burada öncelikle şunu anlatmak gerekiyor;

JavaScript Callback Şablonu

function getProducts(categoryId, callback){
    /* 
       veritabanı işlemine ait satırlar
    */
    callback(result, err);
}

Yukarıdaki gibi bir veritabanı yardımcı metodumuz var diyelim. Burada callback parametresi async işlemleri halletmek için bize gerekiyor. böylece dışarıdan bu metodu çağırdığımızda

dbHelper.getProducts(categoryId, function(data, err){

yukarıdaki gibi içine inline-function, aynı satırda metodu, callback parametresi ile geçebiliyoruz. callback’i hemen burada konsola yazdırsak bize dışarıdaki metodun içeriğini verecek. Daha sonra veritabanı ile ilgili işimiz bittiğinde

callback(result, err)

ile içeriye geçtiğimiz bu metodu çağırıyoruz. Böylece uzun süren veritabanı işleminin bittiğini dışarıya söylüyoruz ve dönen data parametresi productList’e eşitlendiğinde veritabanı işleminin bittiğinden eminiz.

Buradaki problemi incelersek;

var productList = [];
dbHelper.getProducts(categoryId, function(data, err){
    productList = data;
});

for(var i = 0; i < productList.length; i++){
    console.log(productList[i].Name);
}

productList = data satırını async çağırdık fakat biz daha veritabanı işini bitirip bu satırı çağırmadan JavaScript motoru hemen alttaki for döngüsüne girdi bile. Buraya girdiğinde productList’i daha data’ya eşitlemediğimiz için değişkenin içi boş. Döngüyü atlayıp gidiyoruz daha sonra yukarıdaki callback fonksiyonu çalışıyor.

Bunu basitçe çözebiliriz;

dbHelper.getProducts(categoryId, function(data, err){
    var productList = data;
    
    for(var i = 0; i < productList.length; i++){
        console.log(productList[i].Name);
    }
});

for döngüsünü callback metodunun içine aldım. Böylece artık data geldiğinde for döngüsüne girecek. Bu şimdiye kadar çözdüğümüz en basit senaryo.

Callback Hell – Callback Cehennemi

images

Callback Hell

Diyelim ki for döngüsünde de gelen her product için bir işlem yapmaya karar verdik;

dbHelper.getProducts(categoryId, function(data, err){
    var productList = data;
    
    for(var i = 0; i < productList.length; i++){
        dbHelper.updateProductPrice(productList[i].productId, 10,
            function(data, err){
                console.log('product price updated for',
                            data.productId, " new price is: ',
                            data.newPrice);
            }
        );
});

Bu da çalışan başka bir örnek, açıklarsak, önce ürün listesini çektik daha sonra for döngüsü içinde her ürünün productId’sini parametre olarak geçerek %10 zam yaptık diyelim. (Tabi doğrusu tek çağrıda veya sorguda halledebilirdik örnek amaçlı veriyorum)

Gördüğünüz gibi ortalık karışmaya function içinde function kullanmaya  başladık bile. Bunun sebebi, bütün veritabanı işlemlerinin async olması ve eğer bir sonuç loglayacaksak önce yardımcı metodun içine göndermemiz gereken callback metodunu gönderip, veritabanı ile iş bitene kadar bekleyip, bu metodun sonuç ile çağırıldığında loglama işlemimizi yapmamız gerekmesi.

Peki bütün fiyat güncelleme işlemleri bittiğinde birşey yapmak istesek, diyelim ki konsola “bütün fiyat güncellemeleri tamam” yazmak istesek nasıl yaparız?

       dbHelper.updateProductPrice(productList[i].productId, 10,
                function(data, err){
                console.log('product price updated for',
                                 data.productId, " new price is: ',
                                 data.newPrice);
            }
        );

yukarıdaki metod hep async çağırıldığından, yani veritabanına giden sorguların hepsi aynı anda gidip cevap bekledğinden hangi güncellemenin önce biteceğini veya daha doğrusu hangisinin en son bittiğini bilip ona göre bir mantık geliştirmemiz imkansız.

Bu durumda da şöyle bir takla atarak en son cevabı gelen işlemi bulabiliyoruz;

dbHelper.getProducts(categoryId, function(data, err){
    var productList = data;
    var processedCount = 0;

    for(var i = 0; i < productList.length; i++){
        dbHelper.updateProductPrice(productList[i].productId, 10,
            function(data, err){
                console.log('product price updated for',
                            data.productId, " new price is: ',
                            data.newPrice);

                 if(++processedCount === productList.length){
                     console.log('All products are updated');
                 }
            }
        );
});

Yukarıda gördüğümüz gibi processedCount isminde bir değişken ekleyerek her callback metodunda konsola yazdıktan sonra ++processedCount ile bir arttırıp bütün ürün listesi sayısı ile karşılaştırıyorum. Eşit olduğunda konsola bütün ürünler güncellendi mesajı yazarak gerekli mantığı uyguluyorum.

Gerçek hayatta çok daha karışık örnekler karşımıza çıkıyor malesef. Yukarıdaki üç örnek en çok karşılaştığımız problemleri çözüyor.

Tabi bu basit örnekte bile JavaScript kodumuzun okunmasını çok bozan bir durum. Kodu mantıksal metodlara ayırıp biraz okunulurluğu arttırabilseniz bile sonuç pek güzel olmayacak.

callback hell 2

Callback Hell

Burada başka çözümler  ve çözüm kütüphaneleri devreye giriyor;

Promise Yapısı

Yukarıdaki yapıya alternatif olarak npm async.js veya orijinal lokasyonu ile async kütüphanesi. Biraz öğrendikten sonra Promise objelerini kullanarak bize daha temiz bir kod yapısı sunuyor. Birçok JavaScript async kütüphanesi mevcut fakat hepsinin yapısı aşağı yukarı aynı.

Async kütüphanesini Promise yapısını bilmeden de kullanabilirsiniz. Bütün tarayıcılarda ve Node.js üzerinde çalışan en popüler kütüphane bu. Kütüphane içinde 70’e yakın metod var fakat bizim güncel problemlerimizi çözen en önemli metodları parallel, series, waterfall.

Async kütüphanesini inceleyip kullanmaktan korkmamak lazım. JavaScript’in gücü bildiğiniz gibi geliştirilen kütüphanelerden geliyor. Ne kadar çok kütüphaneye alışırsanız programlama hızınız daha çok artıyor, bunlarla ne kadar çok antrenman yapar, proje geliştirirseniz kod problemlerini düşünmekten daha çok süreci düşünmeye başlıyor, daha iş odaklı çalışabiliyorsunuz.

Async kütüphanesini de vakit bulduğumda başka bir yazımda anlatmaya devam edeyim.