Adım Adım Nodejs yazı dizimizin bir önceki bölümünde I/O işlemlerinden yani dosya okuma yazma işlemlerinden bahsettik. Bu bölümde senkron ve asenkron programlamayı anlatacağım.
Senkron Programlama
Yazdığımız programların çoğu yazmış olduğumuz kodları yazılış sırasına göre yukarıdan aşağıya doğru işleyerek ilerler. Her şey sıra ile yapılır. Örneğin bir önceki bölümde yazmış olduğumuz şu kod buna bir örnektir:
var fs=require("fs"); var dosyaIcerigi=fs.readFileSync(process.argv[2]); console.log(“Program devam ediyor...”); var satirlar=dosyaIcerigi.toString().split("\n"); console.log(satirlar.length);
5 satırda sırasıyla işler: fs modülünü çağır, dosya içeriğini al, ekrana “Program devam ediyor…” yaz, satırları hesapla, satır sayısını ekrana yaz. Hiçbir işlem birbirinin önüne geçemez. Ve birbirini beklemek zorundadır. Sırayla, senkron bir şekilde işlemek zorundadır. Bu tip programlama metoduna Senkron Programlama denir.
Asenkron Programlama
Senkron programlamadaki her şeyi sırayla işlemesi ve her bir işlemin birbirini beklemesi yeri geldiğinde programımızı çok yavaşlatabilir, hatta işlem bitene kadar durdurabilir. Örneğin yukarıdaki kodda 3. satır bir önceki satırı yani dosya okuma işlemini beklemek zorundadır. Dosya içeriği çok büyükse bu işlemler dakikalar bile alabilir. Ekrana “Program devam ediyor…” yazdırmak için bir önceki işlemin bitmesini beklemek pek akıllıca değil. İşte bu tip durumlar için asenkron fonksiyonlar kullanırız. Kod akışının sırayla işlemediği, işlemlerin birbirini beklemediği, kod akışının işlem durumlarına göre devam ettiği programlamaya Asenkron Programlama denir.
Asenkron işlemler için modüller içerisinde asenkron fonksiyonlar vardır. Örneğin fs modülü içerisinde kullanmış olduğumuz readFileSync metodunun asenkron versiyonu “readFile” metodudur. Yukarıdaki örneğimiz bu metot ile yeniden yazalım.
var fs=require("fs"); var dosyaIcerigi=fs.readFile(process.argv[2]); console.log(“Program devam ediyor...”) var satirlar=dosyaIcerigi.toString().split("\n"); console.log(satirlar.length);
Bu şekilde kodlarımızı asenkron yapmış olduk. Artık 2. satırdaki işlem çalışırken 3,4 ve 5 satırlar onun sonucunu beklemeden devam eder. Ancak yukarıdaki kodlarımız hata verecek. Neden? Çünkü biz 4. satırda 2. satırdaki kodun sonucunu kullanıyoruz. Ama daha 2. satırdaki kodun işi bitmedi.(Eğer okunan dosya çok çok küçükse küçük bir ihtimal bitmiş olabilir.) Peki biz ikinci satırdaki kodun işinin bittiğini nereden anlayacağız? Bittikten sonra yapmamız gerekenleri nerede belirteceğiz? Callback fonksiyonunda.
Callback fonksiyonu
Asenkron fonksiyonlar içerisindeki işlemler bittikten hemen sonra yapılacakların belirlendiği metotlara callback metotları denir. Bu metotlar asenkron metotlara parametre olarak verilir. Sadece bitiminde değil herhangi bir hata sonucunda da callback fonksiyonları kullanılabilir. Bu tamamen asenkron metodun yapısıyla alakalıdır. Nodejs içerisindeki callback fonksiyonlarında genellikle en az iki tane parametre belirlenir. Biri hata durumunu kontrol etmek için, diğeri fonksiyon sonucunu almak için kullanılır. Callback fonksiyonunun kaç tane parametre aldığını, yapısının nasıl olduğunu, hangisi sıra ile yazılacağını … vs bilgileri kullanacağımız modül ve fonksiyonun dokümantasyonundan öğrenebiliriz. Nodejs içerisindeki resmi modüllerin dokümandasyonuna https://nodejs.org/en/docs/ adresinden kullandığınız nodejs versiyonuna göre ulaşabilirsiniz. Örneğin fs modülü içerisindeki readFile metodunun dokümantasyonuna buradan ulaşabilirsiniz.
Gelelim kullanımına. Yukarıdaki kodu şu şekilde düzenliyoruz:
var fs=require("fs"); var callback1 = function (hata,dosyaIcerigi) { if (hata) { console.log("Bir hata oluştu.") return; } var satirlar=dosyaIcerigi.toString().split("\n"); console.log(satirlar.length); } fs.readFile(process.argv[2],callback1); console.log("Program devam ediyor...");
Kodlarımıza bakacak olursak callback1 adında bir fonksiyon oluşturduk. Bu fonksiyon iki tane parametre alıyor. (Yapısını tabiki dokümantasyondan öğrendik.) Bu fonksiyonu readFile fonksiyonuna parametre olarak verdik. readFile işlemlerini yaptıktan sonra bizim vermiş olduğumuz callback1 fonksiyonunu ilk parametre olarak varsa hatayı, ikinci parametre olarak da hata oluşmamışsa sonucu parametre olarak verecek ve çalıştıracak.
Kodumuza genel olarak tekrar bakarsak, ilk satırda fs modülünü çağırdık. ikindi satırda parametre olarak yolladığımız dosyanın içeriğini okumaya başladık. 2. Satırdaki kodumuz devam ederken 3. satırda ekrana “Program devam ediyor…” yazdık. Diğer taraftan 2. satırdaki kod ne zaman biterse callback1 fonksiyonunu çağıracak. callback1 fonksiyonu içerisinde de ilk olarak hata olup olmadığını kontrol ettik. Varsa konsol ekranına “Bir hata oluştu.” yazdırdık ve programı bitirdik. Yoksa devam edecek. Devam ederse dosyaIcerigi degiskenini string ifadeye çevirip satırlarına ayırdık. Daha sonrada satır sayısını ekrana yazdırdık. Genel olarak programımız bu şekilde işliyor. Aynı programı şu şekilde de yazabiliriz:
var fs = require("fs"); fs.readFile(process.argv[2], function (hata, dosyaIcerigi) { if (hata) { console.log("Bir hata oluştu.") return; } var satirlar = dosyaIcerigi.toString().split("\n"); console.log(satirlar.length); }); console.log("Program devam ediyor...");
Önceki kod ile bu kod arasında hiçbir fark yok. Sadece yazım şekli farklı. İlkinde callback fonksiyonunu bir değişkene atayıp öyle readFile fonksiyonuna ilettik. Diğerinde direkt içerisine yazdık. Hangisi kolayınıza geliyorsa kullanabilirsiniz. İkisinin arasındaki farkı daha iyi anlamak için iki kodu yan yana koyup inceleyebilirsiniz. Javascript ve nodejs dünyasında ikinci kullanım daha yaygındır. Kullanmasanız bile kod okurken ikincisini anlamak faydalı olacaktır.
Gelelim ikinci örneğimize. İkinci bir örnek olarakta dosyanın birinden okuduğumuz içeriği ikinci bir dosyaya yazacağız. Tabi iki dosya isminide parametre olarak alacağız. Okuma işlemi için daha önce kullandığımız fs modülününü readFile fonksiyonunu, yazma işlemi içinde fs modülünün writeFile fonksiyonunu kullanacağız. writeFile fonksiyonuda readFile gibi asenkron bir fonksiyon. (Daha önce kullandığımız writeFileSync fonksiyonunun asenkron versiyonu.) Hem okuma hem yazma işlemlerimiz asenkron olacak. Kodumuz şöyle:
var fs=require("fs"); var callback1 = function (hata,dosyaIcerigi) { if (hata) { console.log("Bir hata oluştu.") return; } fs.writeFile(process.argv[3], dosyaIcerigi, callback2); console.log("Dosya okuma işlemi bitti."); } var callback2=function (hata) { if (hata) { console.log("Bir hata oluştu.") return; } console.log("Dosya yazma işlemi bitti.") } fs.readFile(process.argv[2],callback1); console.log("Program devam ediyor...");
Kodumuzun ne yaptığına bakalım. İlk olarak fs modülünü çağırıyoruz. Sonra callback1 ve callback2 adında iki tane fonksiyon tanımlıyoruz. Daha sonra (20. satırda) dosya okuma işlemine başlanır. Sonra ekrana “Program devam ediyor…” yazdırdık. Dosya okuma işlemi bittikten sonra callback1 fonksiyonu çalışır. callback1 içerisinde öncelikle hata kontrolü yapılır. Hata yoksa writeFile fonksiyonu ile yazma işlemine başlanır. Sonrasında konsol ekranına “Dosya okuma işlemi bitti.” yazdırılır. Yazma işlemi bittikten sonra ise callback2 fonksiyonu çalışır. callback2 içerisinde de illk olarak hata kontrolü yapılır. Daha sonra konsol ekranına “”Dosya yazma işlemi bitti.” yazdırılır. (writeFile fonksiyonunun herhangi bir dönüşü yok.) Daha sonra programımız sonlanır. Yukarıdaki kodu şu şekildede yazabiliriz:
var fs=require("fs"); fs.readFile(process.argv[2],function (hata,dosyaIcerigi) { if (hata) { console.log("Bir hata oluştu.") return; } fs.writeFile(process.argv[3], dosyaIcerigi, function (hata) { if (hata) { console.log("Bir hata oluştu.") return; } console.log("Dosya yazma işlemi bitti.") }); console.log("Dosya okuma işlemi bitti."); }); console.log("Program devam ediyor...");
Bu şekilde kullanım çok yaygın. Ama iç içe iç içe çok fazla fonksiyon gelebilir ve kodun okunurluğu çok düşebilir. Bu tip durumlara Callback Hell yani Callback Cehennemi deniyor. Hangisi kolayınıza geliyorsa kullanabilirsiniz.
İkinci örneğimizde de birden çok asenkron fonksiyonun aynı program içerisinde kullanımı gördük. Yine callback fonksiyonların yada modül içerisindeki fonksiyonların yapısının nasıl olduğunu dökümantasyon üzerinden öğrendik. Peki yazdığımız kodlarda bize callback fonksiyonu yollanmasını nasıl sağlarız? Ve yollanan bu callback fonksiyonları nasıl kullanılır?
Bir sonraki bölümde modüler programlamayı, kendi modüllerimi yazmayı ve kendi modüllerimize callback fonksiyonları yollatıp onları kullanmayı göreceğiz.
Makalede kullanılan kodlara buradan ulaşabilirsiniz.