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.

 

Node.js bir bakışta…

Aslında başlığa aldanmayın benim bu ikinci bakışım, neden mi?

Node.js ilk çıktığında bir JavaScript aşığı olarak bakıp inceleyip biraz oynamıştım. İki üç satırda servis yazabilmek, bunu hemen JSON servis edebilir halde kullanmak çok güzeldi. Node.js daha ilk günden async yapısı ile çok ciddi bir performans sunuyordu hatta compile olan (derlenen) dillerle yarışacak kadar kuvvetliydi.

Daha sonra “herşey bu kadar kolay olamaz” diye düşünüp araştırmalarıma devam ettim ve Node.js’in çok ciddi bir ayıbı olduğunu gördüm. Node.js yük altında bir requeste vermesi gereken cevabı başka bir request’e verebiliyordu. Bu çok ciddi bir sorun çünkü içinde para geçen kritik bir işle uğraştığınızı düşünün başkasının bilgilerini görebiliyor olmanız çok ciddi güvenlik açığı.

İşte bu yüzden ilk bakışımda hemen başarısız sayıp daha ileri gitmedim.

Gelelim ikinci bakışıma;

Geçenlerde bir proje için hem sunucu hem istemci tarafına servis yazmamız gerekti. İstemci tarafı Internet of Things platformlarında çok kullanılan Raspberry PI. Hal böyle olunca node.js tekrar gündeme geldi. Ben bu yazıyı yazarken 5.1.0 versiyonu yayında. Geliştirmeyi de bunun üzerinde yaptım. Yapmadan önce de daha önceki problemleri tekrar araştırdım. Performans testlerini okudum. Dahası kendim örnekler yapıp kodu yazarken ne hissettiğimi göreyim dedim.

Hani bilirsiniz programcı olarak başka dilde de kod yazabilirsiniz, hatta bir süre sonra alışabilirsiniz fakat favori platformunuzun, editörünüzün hissiyatını vermez. Bu durum benim için biraz farklı oldu çünkü öncesinde Angular.js daha sonra da büyük bir Vanilla JavaScript web tabanlı projeden çıkmıştım. Editörüm bu sürede tabi ki sublime text idi ve JavaScript yazmaya çok alışmıştım.

23349677395_56212fd548_z

Kısacası ikinci bakışım şu şekilde;

JavaScript, özellikle dilin async olmayıp ortamın async olmasından kaynaklı hem web tarafında hem node.js üzerinde aynı problemli yapıyı sürdürmeye devam ediyor. Şimdi diyeceksiniz ki “JavaScript async bir dil bizi mi kandırıyorsun?”  Malesef JavaScript bilinenin aksine biraz daha içine girdiğinizde, altyapısını incelediğinizde malesef sync bir dil, yani bütün işlemler tek tek queue’ya attığınız sırayla gerçekleşiyor. Async olarak bildiğiniz bütün yapı AJAX’tan kaynaklanıyor. Bunu da Microsoft zamanında Outlook’un web client’ını yazarken geliştirdi. O kadar güzel yaptılar ki bütün browser’larda standart olarak tanındı.

Malesef sync bir dilde Ajax Call yaptığınızda, örneğin Rest API çağrıları veya sunucu tarafını düşünecek olursak node.js’in veritabanı sorgularını örnek verebiliriz. Node.js platformu, main thread’i boş bırakmak için platform olarak async çalışarak bütün bu çağrıları worker thread’de işliyor. Fakat sizin kodunuz sync çalıştığı için bu işlemin sonucundan haberdar olamıyor. İşte burada JavaScript ortamına alışkın değilseniz, callback veya promise’lerle çalışmaya alışkın değilseniz geliştirdiğiniz yapı baştan kusurulu olabilir. Lokalinizde şans eseri çalışan bu yapı sunucu ortamında, yük altında, yavaşlık oluştuğunda test ettiğinizden farklı davranabilir.

Callback’lerin dezavantajı kodunuzu okunmaz hale çok çabuk getirebilir olması. Malesef bunun node.js ile alakası yok. Projenizde mutlaka promise’ler kullanmaya, başlamadan önce araştırmanızı bu şekilde yapmaya ve geliştireceğiniz eklentileri bunu destekleyecek şekilde seçmeye çalışmanızı öneririm.

Peki hep kötüleri konuştuk, Node.js’in başarısı nedir, neden son 4 yıldır bu kadar ön planda?

Öncelikle JavaScript yukarıda anlattıklarıma rağmen çok güzel bir dil ve JavaScript’le geliştirme yapıyor olmak çok zevkli.

Node.js’in npm installer’ı çok başarılı, bu installer ile indirebileceğiniz eklentiler, arkadaki community çok iyi. JavaScript’te öğrendiğiniz bütün numaraları burada kullanabiliyorsunuz ve underscore.js, moment.js gibi bildik başarılı kütüphaneler burada da kullanılıyor. Hatta HTML üretiyorsanız JQuery’nin sunucu taraflı çalışan klonu da mevcut. Mongodb, SQLite ve aklınıza gelebilecek modern bütün teknolojilerle kolayca çalışabilirsiniz.

Ofiste Mac üzerinde yazdığınız kodu eve gelip Windows üzerinden devam ettirip daha sonrasında da çalışmak üzere Raspberry PI üzerine kurabiliyorsunuz. Her ortamda aynı çalışıyor.

Memory footprint’i dediğimiz çalışırken kullandığı hafıza diğer dillerle karşılaştırdığımızda gayet düşük.

Compiled (derlenen) bir dil olmamasına, interpreted (yorumlanan) bir dil olmasına rağmen performansı çok yüksek. Daha önce C# Web.API ile karşılaştırdığımda kafa kafaya olduğunu görmüştüm, son versiyonun performansı IIS üzerinde çalışan C# projelerinden çok daha iyi, OWIN üzerinde çalışan servislerden biraz daha iyi.

Son olarak, artık Angular.js ile sunucu tarafında Node.js ile JavaScript dışında başka bir dil kullanmadan birçok işinizi halledebiliyorsunuz.

İlk çıktığında ciddi stabilite problemleri yaşasa da artık 4 yıl sonra bugün birçok platformun arka planıda çalışıyor, sürekli geliştiriliyor, desteği çok iyi.

İstediğiniz editörde çalışabiliyorsunuz. Öyle piyasa standardı bir editöre takılıp mutsuz olmak yok veya c#’ta olduğu gibi Visual Studio’nun büyük projelerdeki yavaşlığı, yüksek hafıza kullanımı burada yok. Benim tercihim Sublime Text bu konuda çok başarılı.

Node.js ile ilgili ikinci bakışım bu şekilde. Konuyla ilgili aklımda şunlar da var;

Microsoft’un bütün platformunu Linux ortamına taşımasını bekliyorum. Belki burada Web.API yazmak çok daha keyif verecek ve geliştirdiğim uygulamalar daha performanslı / kabiliyetli olacak.

Herkes gibi Go platformu ile ilk tanışmam da keyifli geçti, bütün örnekleri kısa zamanda yapıp bir iki ufak uygulama geliştirdim. Object Oriented konusunda beni çok mutlu etmedi fakat burada da çok keyifli şeyler yapılıyor, destek günden güne artıyor, artık kendi platformu ile kendi dilinde compiler’ı var ve  bu bir dil için çok büyük bir adım. Go ile çalışan arkadaşlarım da çok keyifli.

Diğer alternatifler mevcut fakat açıkçası ne kadar denediysem beni mutlu etmedi. Ne Python ne Java ne PHP (ApiAgility fena gözükmüyor) ne de Ruby

Son olarak eğer Node.js’e girecekseniz projemde kullandığım sevebileceğiniz birkaç kütüphanenin linkini vereyim;
Generate API Documentation
http://stackoverflow.com/questions/11969542/how-to-generate-api-documentation

MySQL
https://www.npmjs.com/package/mysql

ORM
http://docs.sequelizejs.com/en/latest/
http://sailsjs.org/get-started
http://knexjs.org/

API Development
http://www.smashingmagazine.com/2015/11/sailing-sails-js-mvc-style-framework-node-js/

Node.js Excel Export
https://www.npmjs.com/package/excel-export

Server Side JQuery
https://www.npmjs.com/package/cheerio

datetime
http://momentjs.com/

Jade HTML
http://jade-lang.com/

Node Emailer
https://www.npmjs.com/package/nodemailer

Node Supervisor – Detect Code Changes
https://github.com/petruisfan/node-supervisor

Diğer
http://blog.nodejitsu.com/6-must-have-nodejs-modules/
cradle: A high-level, caching, CouchDB library for Node.js
findit: Walk a directory tree in node.js
node_redis: Redis client for node
node-static: RFC2616 compliant HTTP static-file server module, with built-in caching.
optimist: Light-weight option parsing for node.js
xml2js: Simple XML to JavaScript object converter.

Ve dahası burada npm en çok kullanılanlar listesinde;
https://www.npmjs.com/browse/depended