Türkçe

JavaScript'in yeni Açık Kaynak Yönetimini `using` ve `await using` ile öğrenin. Temizlemeyi otomatikleştirin, kaynak sızıntılarını önleyin ve daha temiz, daha sağlam kod yazın.

JavaScript'in Yeni Süper Gücü: Açık Kaynak Yönetimine Derinlemesine Bir Bakış

Yazılım geliştirmenin dinamik dünyasında, kaynakları etkili bir şekilde yönetmek, sağlam, güvenilir ve performanslı uygulamalar oluşturmanın temel taşıdır. Onlarca yıldır, JavaScript geliştiricileri, dosya tutamaçları, ağ bağlantıları veya veritabanı oturumları gibi kritik kaynakların düzgün bir şekilde serbest bırakılmasını sağlamak için try...catch...finally gibi manuel desenlere güvenmektedir. İşlevsel olmakla birlikte, bu yaklaşım genellikle daha uzun, hataya yatkın ve karmaşık senaryolarda bazen "kader piramidi" olarak adlandırılan bir desen olan yönetilemez hale gelebilir.

Dil için bir paradigma kayması girin: Açık Kaynak Yönetimi (ERM). ECMAScript 2024 (ES2024) standardında tamamlanan bu güçlü özellik, C#, Python ve Java gibi dillerdeki benzer yapılardan esinlenerek, kaynak temizlemeyi yönetmenin bildirimsel ve otomatik bir yolunu sunar. Yeni using ve await using anahtar kelimelerinden yararlanan JavaScript, artık zamansız bir programlama sorununa çok daha zarif ve güvenli bir çözüm sunuyor.

Bu kapsamlı kılavuz, sizi JavaScript'in Açık Kaynak Yönetimi yolculuğuna çıkaracak. Çözdüğü sorunları keşfedeceğiz, temel kavramlarını inceleyeceğiz, pratik örneklerden geçeceğiz ve dünyanın neresinde geliştirme yapıyor olursanız olun, daha temiz, daha dirençli kod yazmanızı sağlayacak gelişmiş desenleri ortaya çıkaracağız.

Eski Muhafız: Manuel Kaynak Temizliğinin Zorlukları

Yeni sistemin zarafetini takdir edebilmemiz için, önce eskisinin acı noktalarını anlamamız gerekir. JavaScript'te kaynak yönetiminin klasik deseni, try...finally bloğudur.

Mantık basittir: Bir kaynağı try bloğunda elde edersiniz ve finally bloğunda serbest bırakırsınız. finally bloğu, try bloğundaki kod başarılı olsa da, başarısız olsa da veya erken dönse de yürütmeyi garanti eder.

Yaygın bir sunucu tarafı senaryosunu düşünelim: Bir dosya açmak, içine bazı veriler yazmak ve ardından dosyanın kapalı olduğundan emin olmak.

Örnek: try...finally ile Basit Bir Dosya İşlemi


const fs = require('fs/promises');

async function processFile(filePath, data) {
  let fileHandle;
  try {
    console.log('Dosya açılıyor...');
    fileHandle = await fs.open(filePath, 'w');
    console.log('Dosyaya yazılıyor...');
    await fileHandle.write(data);
    console.log('Veriler başarıyla yazıldı.');
  } catch (error) {
    console.error('Dosya işleme sırasında bir hata oluştu:', error);
  } finally {
    if (fileHandle) {
      console.log('Dosya kapatılıyor...');
      await fileHandle.close();
    }
  }
}

Bu kod çalışır, ancak birkaç zayıflığı ortaya çıkarır:

Şimdi, bir veritabanı bağlantısı ve bir dosya tutamacına benzer birden fazla kaynağı yönettiğinizi hayal edin. Kod hızla iç içe geçmiş bir karmaşa haline gelir:


async function logQueryResultToFile(query, filePath) {
  let dbConnection;
  try {
    dbConnection = await getDbConnection();
    const result = await dbConnection.query(query);

    let fileHandle;
    try {
      fileHandle = await fs.open(filePath, 'w');
      await fileHandle.write(JSON.stringify(result));
    } finally {
      if (fileHandle) {
        await fileHandle.close();
      }
    }
  } finally {
    if (dbConnection) {
      await dbConnection.release();
    }
  }
}

Bu iç içe geçme, bakımı ve ölçeklendirilmesi zordur. Daha iyi bir soyutlamaya ihtiyaç duyulduğunun açık bir işaretidir. Bu, Açık Kaynak Yönetiminin çözmek için tasarlandığı tam olarak sorundur.

Bir Paradigma Değişimi: Açık Kaynak Yönetimi İlkeleri

Açık Kaynak Yönetimi (ERM), bir kaynak nesnesi ile JavaScript çalışma zamanı arasında bir sözleşme sunar. Temel fikir basittir: Bir nesne, nasıl temizlenmesi gerektiğini bildirebilir ve dil, nesne kapsam dışına çıktığında bu temizliği otomatik olarak gerçekleştirmek için sözdizimi sağlar.

Bu, iki ana bileşen aracılığıyla elde edilir:

  1. Disposable Protokolü: Nesnelerin özel sembolleri kullanarak kendi temizleme mantıklarını tanımlamaları için standart bir yol: Eşzamanlı temizleme için Symbol.dispose ve eşzamansız temizleme için Symbol.asyncDispose.
  2. `using` ve `await using` Bildirimleri: Bir kaynağı bir blok kapsamına bağlayan yeni anahtar kelimeler. Bloktan çıkıldığında, kaynağın temizleme yöntemi otomatik olarak çağrılır.

Temel Kavramlar: `Symbol.dispose` ve `Symbol.asyncDispose`

ERM'nin kalbinde, iki yeni iyi bilinen Sembol bulunur. Anahtar olarak bu sembollerden biriyle bir yönteme sahip bir nesne, bir "atılabilir kaynak" olarak kabul edilir.

`Symbol.dispose` ile Eşzamanlı İmha

Symbol.dispose sembolü, eşzamanlı bir temizleme yöntemi belirtir. Bu, eşzamanlı olarak bir dosya tutamacını kapatmak veya bir bellek içi kilidini serbest bırakmak gibi, temizlemenin herhangi bir eşzamansız işlem gerektirmediği kaynaklar için uygundur.

Kendini temizleyen geçici bir dosya için bir sarmalayıcı oluşturalım.


const fs = require('fs');
const path = require('path');

class TempFile {
  constructor(content) {
    this.path = path.join(__dirname, `temp_${Date.now()}.txt`);
    fs.writeFileSync(this.path, content);
    console.log(`Geçici dosya oluşturuldu: ${this.path}`);
  }

  // Bu, eşzamanlı atılabilir yöntemdir
  [Symbol.dispose]() {
    console.log(`Geçici dosya imha ediliyor: ${this.path}`);
    try {
      fs.unlinkSync(this.path);
      console.log('Dosya başarıyla silindi.');
    } catch (error) {
      console.error(`Dosya silinemedi: ${this.path}`, error);
      // Ayrıca dispose içinde hataları işlemek önemlidir!
    }
  }
}

TempFile'ın herhangi bir örneği artık atılabilir bir kaynaktır. Diskteki dosyayı silme mantığını içeren, Symbol.dispose tarafından anahtarlanmış bir yönteme sahiptir.

`Symbol.asyncDispose` ile Eşzamansız İmha

Birçok modern temizleme işlemi eşzamansızdır. Bir veritabanı bağlantısını kapatmak, ağ üzerinden bir QUIT komutu göndermeyi gerektirebilir veya bir mesaj kuyruğu istemcisinin giden arabelleğini temizlemesi gerekebilir. Bu senaryolar için Symbol.asyncDispose kullanırız.

Symbol.asyncDispose ile ilişkili yöntem bir Promise döndürmelidir (veya bir async fonksiyonu olmalıdır).

Eşzamansız olarak bir havuza geri bırakılması gereken bir sahte veritabanı bağlantısı modelleyelim.


// Bir sahte veritabanı havuzu
const mockDbPool = {
  getConnection: () => {
    console.log('Veritabanı bağlantısı alındı.');
    return new MockDbConnection();
  }
};

class MockDbConnection {
  query(sql) {
    console.log(`Sorgu yürütülüyor: ${sql}`);
    return Promise.resolve({ success: true, rows: [] });
  }

  // Bu, eşzamansız atılabilir yöntemdir
  async [Symbol.asyncDispose]() {
    console.log('Veritabanı bağlantısı havuza geri bırakılıyor...');
    // Bağlantıyı serbest bırakmak için bir ağ gecikmesini simüle edin
    await new Promise(resolve => setTimeout(resolve, 50));
    console.log('Veritabanı bağlantısı serbest bırakıldı.');
  }
}

Şimdi, herhangi bir MockDbConnection örneği bir async atılabilir kaynaktır. Artık ihtiyacınız olmadığında, kendisini eşzamansız olarak nasıl serbest bırakacağını bilir.

Yeni Sözdizimi: `using` ve `await using` İşlemde

Atılabilir sınıflarımız tanımlandığına göre, bunları otomatik olarak yönetmek için yeni anahtar kelimeleri kullanabiliriz. Bu anahtar kelimeler, let ve const gibi blok kapsamlı bildirimler oluşturur.

`using` ile Eşzamanlı Temizleme

using anahtar kelimesi, Symbol.dispose uygulayan kaynaklar için kullanılır. Kod yürütmesi, using bildiriminin yapıldığı bloğu terk ettiğinde, [Symbol.dispose]() yöntemi otomatik olarak çağrılır.

TempFile sınıfımızı kullanalım:


function processDataWithTempFile() {
  console.log('Bloğa giriliyor...');
  using tempFile = new TempFile('Bu bazı önemli verilerdir.');

  // Burada tempFile ile çalışabilirsiniz
  const content = fs.readFileSync(tempFile.path, 'utf8');
  console.log(`Geçici dosyadan okundu: "${content}"`);

  // Burada temizleme koduna gerek yok!
  console.log('...daha fazla çalışma...');
} // <-- tempFile.[Symbol.dispose]() tam da burada otomatik olarak çağrılır!

processDataWithTempFile();
console.log('Bloktan çıkıldı.');

Çıktı şöyle olacaktır:

Bloğa giriliyor...
Geçici dosya oluşturuldu: /path/to/temp_1678886400000.txt
Geçici dosyadan okundu: "Bu bazı önemli verilerdir."
...daha fazla çalışma...
Geçici dosya imha ediliyor: /path/to/temp_1678886400000.txt
Dosya başarıyla silindi.
Bloktan çıkıldı.

Bunun ne kadar temiz olduğuna bakın! Kaynağın tüm yaşam döngüsü blok içinde yer almaktadır. Onu bildiririz, kullanırız ve unuturuz. Dil temizlemeyi halleder. Bu, okunabilirlik ve güvenlikte büyük bir gelişmedir.

Birden Fazla Kaynağı Yönetme

Aynı blokta birden fazla using bildirimi yapabilirsiniz. Bunlar, oluşturulma sıralarının tersine göre (bir LIFO veya "yığın benzeri" davranış) imha edilecektir.


{
  using resourceA = new MyDisposable('A'); // İlk oluşturuldu
  using resourceB = new MyDisposable('B'); // İkinci oluşturuldu
  console.log('Blok içinde, kaynaklar kullanılıyor...');
} // önce resourceB imha edilir, sonra resourceA

`await using` ile Eşzamansız Temizleme

await using anahtar kelimesi, using'in eşzamansız karşılığıdır. Symbol.asyncDispose uygulayan kaynaklar için kullanılır. Temizleme eşzamansız olduğundan, bu anahtar kelime yalnızca bir async fonksiyonu içinde veya bir modülün en üst düzeyinde (en üst düzey await destekleniyorsa) kullanılabilir.

MockDbConnection sınıfımızı kullanalım:


async function performDatabaseOperation() {
  console.log('Async fonksiyonuna giriliyor...');
  await using db = mockDbPool.getConnection();

  await db.query('SELECT * FROM users');

  console.log('Veritabanı işlemi tamamlandı.');
} // <-- await db.[Symbol.asyncDispose]() burada otomatik olarak çağrılır!

(async () => {
  await performDatabaseOperation();
  console.log('Async fonksiyonu tamamlandı.');
})();

Çıktı, eşzamansız temizlemeyi gösterir:

Async fonksiyonuna giriliyor...
Veritabanı bağlantısı alındı.
Sorgu yürütülüyor: SELECT * FROM users
Veritabanı işlemi tamamlandı.
Veritabanı bağlantısı havuza geri bırakılıyor...
(50ms bekler)
Veritabanı bağlantısı serbest bırakıldı.
Async fonksiyonu tamamlandı.

Tıpkı using'de olduğu gibi, await using sözdizimi tüm yaşam döngüsünü yönetir, ancak eşzamansız temizleme işlemini doğru bir şekilde bekler. Hatta yalnızca eşzamanlı olarak atılabilir kaynakları bile işleyebilir; onları beklemeyecektir.

Gelişmiş Desenler: `DisposableStack` ve `AsyncDisposableStack`

Bazen, using'in basit blok kapsamı yeterince esnek değildir. Ya tek bir sözcüksel bloğa bağlı olmayan bir ömre sahip bir kaynak grubunu yönetmeniz gerekiyorsa? Veya Symbol.dispose ile nesneler üretmeyen eski bir kitaplıkla entegre oluyorsanız?

Bu senaryolar için JavaScript iki yardımcı sınıf sağlar: DisposableStack ve AsyncDisposableStack.

`DisposableStack`: Esnek Temizleme Yöneticisi

DisposableStack, bir temizleme işlemleri koleksiyonunu yöneten bir nesnedir. Kendisi de atılabilir bir kaynaktır, bu nedenle tüm yaşam döngüsünü bir using bloğuyla yönetebilirsiniz.

Birkaç kullanışlı yönteme sahiptir:

Örnek: Koşullu Kaynak Yönetimi

Yalnızca belirli bir koşul yerine getirilirse bir günlük dosyası açan, ancak tüm temizlemenin sonunda tek bir yerde gerçekleşmesini isteyen bir fonksiyon hayal edin.


function processWithConditionalLogging(shouldLog) {
  using stack = new DisposableStack();

  const db = stack.use(getDbConnection()); // Her zaman veritabanını kullan

  if (shouldLog) {
    const logFileStream = fs.createWriteStream('app.log');
    // Akış için temizlemeyi ertele
    stack.defer(() => {
      console.log('Günlük dosyası akışı kapatılıyor...');
      logFileStream.end();
    });
    db.logTo(logFileStream);
  }

  db.doWork();

} // <-- Yığın imha edilir ve kayıtlı tüm temizleme fonksiyonlarını LIFO sırasına göre çağırır.

`AsyncDisposableStack`: Eşzamansız Dünya İçin

Tahmin edebileceğiniz gibi, AsyncDisposableStack eşzamansız versiyondur. Hem eşzamanlı hem de eşzamansız atılabilirleri yönetebilir. Birincil temizleme yöntemi, tüm eşzamansız temizleme işlemleri tamamlandığında çözülen bir Promise döndüren .disposeAsync()'dir.

Örnek: Kaynakların Bir Karışımını Yönetme

Bir veritabanı bağlantısına (async temizleme) ve geçici bir dosyaya (sync temizleme) ihtiyacı olan bir web sunucusu istek işleyicisi oluşturalım.


async function handleRequest() {
  await using stack = new AsyncDisposableStack();

  // Bir async atılabilir kaynağı yönet
  const dbConnection = await stack.use(getAsyncDbConnection());

  // Bir sync atılabilir kaynağı yönet
  const tempFile = stack.use(new TempFile('istek verileri'));

  // Eski bir API'den bir kaynak al
  const legacyResource = getLegacyResource();
  stack.adopt(legacyResource, () => legacyResource.shutdown());

  console.log('İstek işleniyor...');
  await doWork(dbConnection, tempFile.path);

} // <-- stack.disposeAsync() çağrılır. Async temizlemeyi doğru bir şekilde bekleyecektir.

AsyncDisposableStack, karmaşık kurulum ve sökme mantığını temiz, öngörülebilir bir şekilde düzenlemek için güçlü bir araçtır.

`SuppressedError` ile Sağlam Hata İşleme

ERM'nin en ince ancak önemli iyileştirmelerinden biri, hataları nasıl işlediğidir. using bloğu içinde bir hata atılırsa ve *başka* bir hata sonraki otomatik imha sırasında atılırsa ne olur?

Eski try...finally dünyasında, finally bloğundan gelen hata genellikle try bloğundan gelen orijinal, daha önemli hatayı geçersiz kılar veya "bastırır". Bu genellikle hata ayıklamayı inanılmaz derecede zorlaştırdı.

ERM bunu, yeni bir genel hata türüyle çözer: `SuppressedError`. Başka bir hata zaten yayılıyorsa, imha sırasında bir hata oluşursa, imha hatası "bastırılır". Orijinal hata atılır, ancak artık imha hatasını içeren bir suppressed özelliğine sahiptir.


class FaultyResource {
  [Symbol.dispose]() {
    throw new Error('İmha sırasında hata!');
  }
}

try {
  using resource = new FaultyResource();
  throw new Error('İşlem sırasında hata!');
} catch (e) {
  console.log(`Yakalanan hata: ${e.message}`); // İşlem sırasında hata!
  if (e.suppressed) {
    console.log(`Bastırılan hata: ${e.suppressed.message}`); // İmha sırasında hata!
    console.log(e instanceof SuppressedError); // false
    console.log(e.suppressed instanceof Error); // true
  }
}

Bu davranış, kritik hata bağlamını asla kaybetmemenizi sağlayarak, çok daha sağlam ve hata ayıklanabilir sistemlere yol açar.

JavaScript Ekosisteminde Pratik Kullanım Alanları

Açık Kaynak Yönetiminin uygulamaları, arka uç, ön uç veya test üzerinde çalışan geliştiriciler için çok geniştir ve tüm dünya için geçerlidir.

Tarayıcı ve Çalışma Zamanı Desteği

Modern bir özellik olarak, Açık Kaynak Yönetimini nerede kullanabileceğinizi bilmek önemlidir. 2023 sonu / 2024 başı itibarıyla, temel JavaScript ortamlarının en son sürümlerinde destek yaygındır:

Daha eski ortamlar için, using sözdizimini dönüştürmek ve gerekli sembolleri ve yığın sınıflarını doldurmak için uygun eklentilere sahip Babel gibi derleyicilere güvenmeniz gerekir.

Sonuç: Yeni Bir Güvenlik ve Netlik Çağı

JavaScript'in Açık Kaynak Yönetimi, yalnızca sözdizimsel şekerden daha fazlasıdır; güvenliği, netliği ve bakımı teşvik eden, dilin temel bir iyileştirmesidir. Kaynak temizlemenin zahmetli ve hataya açık sürecini otomatikleştirerek, geliştiricileri birincil iş mantıklarına odaklanmakta özgür bırakır.

Temel çıkarımlar şunlardır:

Yeni projelere başlarken veya mevcut kodu yeniden düzenlerken, bu güçlü yeni deseni benimsemeyi düşünün. JavaScript'inizi daha temiz, uygulamalarınızı daha güvenilir ve geliştirici olarak hayatınızı biraz daha kolay hale getirecektir. Modern, profesyonel JavaScript yazmak için gerçekten küresel bir standarttır.