Modul Berbasis IIFE

Good authors divide their books into chapters; good programmers divide their programs into modules.

Secara kebahasaan, modul dapat diartikan sebagai komponen dari suatu sistem yang berdiri sendiri tetapi menunjang program dari sistem itu [1]. Modul, dalam pemrograman, membagi kode suatu program menjadi beberapa bagian yang dikelompokkan berdasarkan kriteria tertentu. Membagi suatu program menjadi beberapa modul memiliki kelebihan yang sama dengan membagi buku menjadi beberapa bab: mendefinisikan struktur yang jelas dari sebuah sistem.

Dengan modul, bagian-bagian dari program menjadi lebih mudah diidentifikasi berdasarkan kriteria yang ditetapkan, seringkali juga dilengkapi dengan namespace-nya masing-masing. Namespacing ini penting di Javascript karena potensi konflik penamaan di global scope. Bahkan, sangat dianjurkan untuk membatasi penggunaan variabel global di Javascript untuk menghindari polusi global scope, yang dapat merusak performa program Javascript kita [1:1].

Modularisasi dalam Javascript

Terkait dengan usaha namespacing di Javascript, mungkin yang akan kita pikirkan pertama kali adalah mengimplementasi namespace dengan objek.

var Recipes = {
  riceAmount: '1 piring',
  soyAmount: '2 sendok makan',

  preheatOil: function() {
    console.log('Panaskan minyak.');
  },
  addRice: function() {
    console.log('Tambahkan ' + this.riceAmount + ' nasi.')
  },
  addSoySauce: function() {
    console.log('Tambahkan ' + this.soyAmount + ' kecap.')
  },

  friedRice: function() {
    this.preheatOil();
    this.addRice();
    this.addSoySauce();
  }
}

Recipes.friedRice(); // Panaskan minyak. Tambahkan 1 piring nasi. Tambahkan 2 sendok makan kecap.

Memang dengan namespacing berbasis objek ini kita hanya perlu menyimpan Recipes di global scope, tetapi implikasinya adalah kita dapat mengakses seluruh isi Recipes dari tempat yang tidak seharusnya.

Recipes.riceAmount = '2 kontainer';
Recipes.soyAmount = '1 ember';
Recipes.friedRice(); // Panaskan minyak. Tambahkan 2 kontainer nasi. Tambahkan 1 ember kecap.

Idealnya, riceAmount dan soyAmount hanya dapat diakses oleh friedRice agar tidak mengacaukan resep nasi goreng yang sudah didefinisikan di awal. Ilustrasi tadi menunjukkan bahwa namespacing menggunakan objek belum dapat memenuhi principle of least privilege [1:2]. Untuk dapat memenuhinya, kita harus bisa membuat informasi-informasi selain resep masakan pada Recipe tidak dapat diakses dari luar modul tersebut. Dengan kata lain, kita harus membuat informasi-informasi tersebut private untuk modul 'Recipes'. Pembuatan variabel private (atau dikenal juga sebagai variabel lokal) dalam Javascript dilakukan memasukkan kode yang ingin disembuyikan ke dalam sebuah fungsi.

Function Block Scoping

Pembatasan scope di dalam Javascript dilakukan berdasarkan function block. Artinya, semua variabel yang didefinisikan di dalam blok sebuah fungsi hanya dapat diakses dari dalam blok fungsi itu sendiri.

function greet() {
  var greeting = "Hello!";
  console.log(greeting);
}

greet(); // Hello!
console.log(greeting); // ReferenceError: greet is not defined

Pada kode ini, kita berhasil membuat greeting menjadi sebuah variabel privat untuk fungsi greet. Sekarang kita terapkan teknik yang sama ke modul Recipes tadi.

function Recipes() {
  var riceAmount = '1 piring';
  var soyAmount = '2 sendok makan';

  function preheatOil() {
    console.log('Panaskan minyak.');
  }

  function addRice() {
    console.log('Tambahkan ' + riceAmount + ' nasi.');
  }

  function addSoySauce() {
    console.log('Tambahkan ' + soyAmount + ' kecap.');
  }

  function friedRice() {
    preheatOil();
    addRice();
    addSoySauce();
  }

  friedRice();
}

Recipes(); // Panaskan minyak. Tambahkan 1 piring nasi. Tambahkan 2 sendok makan kecap.
console.log(riceAmount); // ReferenceError: riceAmount is not defined
console.log(soyAmount); // ReferenceError: soyAmount is not defined

Pada kode di atas, dapat didemonstrasikan bahwa sekarang riceAmount dan soyAmount sudah menjadi variabel privat untuk Recipes.

Closure

Pada contoh kode terakhir, fungsi Recipes menyediakan fungsionalitas dengan mengeksekusi fungsi internalnya. Implikasi dari model tersebut adalah fungsi tersebut akan selesai digunakan setelah dipanggil (Recipes()) sehingga tidak dapat menyimpan state, suatu kemampuan yang dibutuhkan oleh sebuah modul.

Untuk mendemonstrasikannya, saya akan mengubah fungsionalitas friedRice menjadi sebuah counter yang menghitung berapa kali nasi goreng dimasak, dengan bantuan variabel baru counter.

function Recipes() {
  var counter = 0;

  function count() {
    count++;
  }

  function friedRice() {
    console.log('Nasi goreng telah dimasak' + counter + 'kali');   
  }

  friedRice();
}

Recipes(); // Nasi goreng telah dimasak 1 kali
Recipes(); // Nasi goreng telah dimasak 1 kali
Recipes(); // Nasi goreng telah dimasak 1 kali

Dengan model yang sekarang, Recipes tidak dapat menyimpan state karena setelah eksekusi fungsinya selesai, fungsi tersebut langsung dibersihkan oleh garbage collector. Ketika kita perlu dapat menyimpan variabel-variabel state di dalam suatu fungsi, kita perlu memanfaatkan closure.

Closure adalah saat sebuah fungsi dapat mengingat dan mengakses scope-nya meskipun informasi di dalam scope tersebut dipanggil dari luar scope itu sendiri[1:3]. Mari kita modifikasi module Recipes terakhir kita sehingga ia mengembalikan fungsi friedRice alih-alih menjalankannya.

function Recipes() {
  var riceAmount = '1 piring';
  var soyAmount = '2 sendok makan';

  function preheatOil() {
    console.log('Panaskan minyak.');
  }

  function addRice() {
    console.log('Tambahkan ' + riceAmount + ' nasi.');
  }

  function addSoySauce() {
    console.log('Tambahkan ' + soyAmount + ' kecap.');
  }

  function friedRice() {
    preheatOil();
    addRice();
    addSoySauce();
  }

  return friedRice;
}

var foo = Recipes();
foo(); // Panaskan minyak. Tambahkan 1 piring nasi. Tambahkan 2 sendok makan kecap.

Pada kode ini, ketika fungsi Recipes sudah selesai dijalankan dan disimpan ke dalam variabel foo, intuisi kita mungkin akan menganggap variabel-variabel dan fungsi-fungsi di dalam instance Recipes yang barusan dijalankan akan segera dihapus dari memori oleh garbage collector. Namun, seperti yang dapat kita lihat pada saat eksekusi foo(), semua variabel dan fungsi itu masih tersimpan di memori dan berjalan dengan baik.

Dengan adanya closure, kita dapat dengan efektif menyimpan state di dalam variabel-variabel local/private dan menyediakan antarmuka yang fleksibel lewat return value sebuah fungsi.

Pola Modul dengan IIFE

Dalam contoh kode terakhir, fungsi Recipes kita telah menerapkan privasi pada logika internalnya, dan hanya menyediakan antarmuka berupa nilai kembaliannya yang berupa fungsi friedRice untuk mencetak resep nasi goreng. Namun, pada dasarnya modul Recipes kita dimaksudkan untuk menjadi repositori resep berbagai makanan, kita harus mengubah desainnya untuk bisa menampung lebih dari sekadar friedRice saja.

Katakanlah saya ingin program saya bisa memberikan user resep mie goreng, maka hal yang paling logis untuk saya lakukan adalah menambahkan fungsionalitas friedNoodles ke dalam modul Recipes. Hal ini bisa kita capai mengingat dua hal: 1) sebuah fungsi dapat memberikan objek sebagai return value-nya; 2) dengan berjalannya closure, objek yang dikembalikan fungsi tersebut dapat mengingat dan mengakses variabel-variabel privatnya.

function Recipes() {
  var riceAmount = '1 piring';
  var noodleAmount = '1 mangkok';
  var soyAmount = '2 sendok makan';

  function preheatOil() {
    console.log('Panaskan minyak.');
  }

  function addRice() {
    console.log('Tambahkan ' + riceAmount + ' nasi.');
  }

  function addNoodles() {
    console.log('Tambahkan ' + noodleAmount + ' mie.');
  }

  function addSoySauce() {
    console.log('Tambahkan ' + soyAmount + ' kecap.');
  }

  return {
    friedRice: function() {
      preheatOil();
      addRice();
      addSoySauce();
    },
    friedNoodles: function() {
      preheatOil();
      addNoodles();
      addSoySauce();
    }
  }
}

var recipes = Recipes();
recipes.friedRice(); // Panaskan minyak. Tambahkan 1 piring nasi. Tambahkan 2 sendok makan kecap.
recipes.friedNoodles(); // Panaskan minyak. Tambahkan 1 mangkok mie. Tambahkan 2 sendok makan kecap.

Dengan kemampuan menyediakan antarmuka yang paripurna seperti di atas, fungsi Recipes kita secara efektif sudah merupakan sebuah modul. Namun, masih terdapat satu permasalahan seperti yang bisa disaksikan di atas: kita menggunakan dua variabel pada global scope, Recipes yang merupakan konsekuensi dari deklarasi fungsi (function Recipes() { /*...*/ }) dan recipes yang dibuat untuk menampung Recipes agar dapat dipanggil.

Di sinilah kita dapat gunakan Immediately-Invoked Function Expression (IIFE) untuk memangkas penggunaan variabel global. Kita bisa membuat function declaration Recipes menjadi sebuah IIFE dengan mengurungnya di dalam () seperti ini.

(function Recipes() {
  /* ... */
})();

Modul tersebut menggunakan 0 global variable. Masalahnya, kita jadi tidak memiliki referensi ke modul tersebut, sehingga modul tersebut jadi tak berguna. Kesimpulannya, untuk menjadi modul yang paripurna, kita perlu menyimpan fungsi IIFE tersebut di dalam sebuah variabel untuk kepentingan referensi.

/*
** NOTE: kurung di antara function declaration
** Recipes sebenarnya opsional, tapi dianjurkan
** untuk tetap ditulis untuk kepentingan readability
** lengkapnya lihat catatan kaki #5
*/
var recipes = (function Recipes() {
  /* ... */
})();

recipes.friedRice();
recipes. friedNoodles();

Jadilah sebuah modul berbasis IIFE yang dapat digunakan dalam seluruh program kita lewat antarmuka berupa objek yang dikembalikannya.



  1. Immediately-Invoked Function Expression dan Motivasinya ↩︎ ↩︎ ↩︎ ↩︎