JAVASCRIPT ECMASCRIPT DECORATORS PROGRAMMING-LANGUAGE WEB-DEVELOPMENT CLEAN-CODE DESIGN-PATTERNS DEVELOPER-EXPERIENCE FRONTEND BACKEND METAPROGRAMMING WEB-STANDARDS PRODUCTIVITY

ECMAScript Decorators: Membangun Kode yang Lebih Bersih dan Fleksibel dengan Standar JavaScript Terbaru

⏱️ 9 menit baca
👨‍💻

ECMAScript Decorators: Membangun Kode yang Lebih Bersih dan Fleksibel dengan Standar JavaScript Terbaru

1. Pendahuluan

Pernahkah Anda merasa kode JavaScript Anda dipenuhi dengan logika berulang, seperti logging, validasi, atau otorisasi, yang tersebar di banyak tempat? Atau mungkin Anda ingin menambahkan fungsionalitas ke kelas atau metode tanpa mengubah definisi aslinya secara langsung? Jika ya, maka ECMAScript Decorators adalah fitur yang mungkin sudah Anda tunggu-tunggu!

Decorators adalah fitur JavaScript yang memungkinkan kita untuk menambahkan metadata atau memodifikasi perilaku kelas, metode, properti, atau pengakses (getter/setter) secara deklaratif. Bayangkan Anda punya sebuah “stiker ajaib” yang bisa Anda tempelkan ke bagian-bagian kode Anda untuk memberinya kemampuan baru atau mengubahnya sedikit. Nah, itulah decorators!

Kenapa Decorators Penting?

Decorators datang untuk memecahkan beberapa masalah umum dalam pengembangan perangkat lunak:

  1. Mengurangi Boilerplate: Logika yang sering diulang (seperti logging, caching, validasi) bisa diekstraksi ke dalam decorator, membuat kode utama lebih bersih dan fokus pada logika bisnis.
  2. Meningkatkan Keterbacaan: Dengan sintaksis deklaratif (@namaDecorator), Anda bisa langsung melihat fungsionalitas tambahan yang diterapkan pada sebuah elemen.
  3. Reusability: Decorators bisa digunakan kembali di berbagai kelas atau metode, mendorong pola Don’t Repeat Yourself (DRY).
  4. Fleksibilitas dan Ekstensibilitas: Memungkinkan Anda untuk memperpanjang fungsionalitas tanpa menggunakan pewarisan yang seringkali kaku atau mengubah kode sumber asli.

💡 Catatan Penting: Perbedaan dengan TypeScript Decorators Jika Anda sudah familiar dengan TypeScript, Anda mungkin sudah tahu tentang Decorators di sana. ECMAScript Decorators, yang akan kita bahas di artikel ini, adalah standar baru yang sedang dalam proses finalisasi (saat artikel ini ditulis, sudah mencapai Stage 3 dan hampir final). Meskipun konsepnya mirip, ada perbedaan sintaksis dan perilaku API yang signifikan antara implementasi TypeScript yang lama (yang bersifat eksperimental) dengan standar ECMAScript ini. Artikel ini akan fokus pada standar ECMAScript yang akan segera menjadi bagian resmi dari JavaScript.

Fitur ini sudah didukung oleh Babel dan akan segera tersedia secara native di browser dan runtime JavaScript. Mari kita selami bagaimana decorators bekerja dan bagaimana kita bisa memanfaatkannya!

2. Memahami Dasar-dasar ECMAScript Decorators

Sintaks dasar decorator sangat sederhana: Anda menempatkan tanda @ diikuti dengan nama fungsi decorator di atas elemen yang ingin Anda modifikasi.

// Contoh fungsi decorator sederhana
function log(value, context) {
  console.log(`Meng-decorate: ${context.kind} - ${context.name}`);
  // Decorator bisa mengembalikan nilai baru untuk elemen yang di-decorate
  // atau membiarkan aslinya jika tidak mengembalikan apa-apa
}

@log
class MyClass {
  @log
  myMethod() {
    console.log("Metode myMethod dijalankan.");
  }

  @log
  myProperty = 42;
}

const instance = new MyClass();
instance.myMethod();
// Output:
// Meng-decorate: class - MyClass
// Meng-decorate: method - myMethod
// Meng-decorate: field - myProperty
// Metode myMethod dijalankan.

Dalam contoh di atas, @log adalah decorator yang diaplikasikan pada MyClass, myMethod, dan myProperty. Fungsi log akan dipanggil oleh runtime JavaScript saat definisi elemen-elemen tersebut diproses.

Setiap fungsi decorator menerima dua argumen:

  1. value: Nilai elemen yang di-decorate (misalnya, definisi kelas itu sendiri, definisi metode, atau nilai properti).
  2. context: Objek yang berisi metadata tentang elemen yang di-decorate, seperti kind (jenis elemen: ‘class’, ‘method’, ‘field’, ‘getter’, ‘setter’), name (nama elemen), access (untuk properti/metode), dan fungsi utilitas lainnya.

3. Decorator untuk Kelas (Class Decorators)

Class decorators adalah cara yang ampuh untuk memodifikasi atau memperluas definisi sebuah kelas. Anda bisa menggunakannya untuk menambahkan properti baru, metode, bahkan mengubah konstruktor kelas.

📌 Use Case: Menambahkan properti statis createdAt ke setiap kelas yang di-decorate.

function addCreationTimestamp(value, context) {
  if (context.kind === 'class') {
    // Mengembalikan kelas baru atau kelas yang dimodifikasi
    return class extends value {
      static createdAt = new Date().toISOString();
      constructor(...args) {
        super(...args);
        console.log(`Instance dari ${context.name} dibuat pada ${this.constructor.createdAt}`);
      }
    };
  }
}

@addCreationTimestamp
class UserService {
  constructor(name) {
    this.name = name;
  }
  getUserInfo() {
    return `User: ${this.name}`;
  }
}

@addCreationTimestamp
class ProductService {
  constructor(productName) {
    this.productName = productName;
  }
  getProductDetails() {
    return `Product: ${this.productName}`;
  }
}

console.log(UserService.createdAt); // Output: (ISO timestamp)
const user = new UserService("Budi"); // Output: Instance dari UserService dibuat pada (ISO timestamp)
console.log(ProductService.createdAt); // Output: (ISO timestamp)
const product = new ProductService("Laptop"); // Output: Instance dari ProductService dibuat pada (ISO timestamp)

Dalam contoh ini, addCreationTimestamp adalah class decorator yang menambahkan properti statis createdAt ke kelas dan juga memodifikasi konstruktor untuk mencetak pesan. Ini adalah cara yang bersih untuk menambahkan fungsionalitas lintas-kelas tanpa perlu pewarisan eksplisit atau memodifikasi setiap kelas secara manual.

4. Decorator untuk Metode (Method Decorators)

Method decorators memungkinkan Anda untuk mengintersep atau memodifikasi perilaku sebuah metode. Ini sangat berguna untuk logika seperti caching, logging, validasi input, atau otorisasi.

🎯 Contoh Konkret: Membuat decorator debounce untuk mencegah metode dipanggil terlalu sering.

function debounce(delayMs) {
  return function(value, context) {
    if (context.kind === 'method') {
      let timeoutId;
      return function(...args) { // Ini adalah metode yang dimodifikasi
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
          value.apply(this, args); // Panggil metode asli
        }, delayMs);
      };
    }
  };
}

class SearchComponent {
  constructor() {
    this.searchTerm = '';
  }

  @debounce(500) // Panggil searchAPI hanya jika tidak ada input selama 500ms
  search(term) {
    this.searchTerm = term;
    console.log(`Mencari untuk: ${this.searchTerm}...`);
    // Bayangkan di sini ada panggilan ke API
  }
}

const searchInput = new SearchComponent();
searchInput.search("a");
searchInput.search("ap");
searchInput.search("app"); // Setelah 500ms, hanya ini yang akan dieksekusi
// Output setelah 500ms: Mencari untuk: app...

Dalam contoh debounce, decorator debounce(500) mengembalikan sebuah fungsi decorator. Fungsi decorator ini kemudian menerima metode search dan context-nya, lalu mengembalikan metode baru yang telah di-debounce. Ini adalah pola yang sangat kuat untuk mengelola interaksi pengguna atau panggilan API yang berulang.

5. Decorator untuk Properti dan Getter/Setter

Selain kelas dan metode, decorators juga bisa diterapkan pada properti kelas dan pengakses (getter/setter). Ini membuka peluang untuk menambahkan validasi, observabilitas, atau bahkan membuat properti menjadi read-only.

Use Case: Membuat properti read-only.

function readonly(value, context) {
  if (context.kind === 'field') {
    return function(initialValue) {
      // Mengembalikan properti dengan getter saja
      Object.defineProperty(this, context.name, {
        value: initialValue,
        writable: false, // Membuat properti tidak bisa ditulis ulang
        configurable: context.static ? true : false,
        enumerable: context.static ? true : false,
      });
    };
  }
}

class Config {
  @readonly
  API_KEY = "super-secret-key";

  @readonly
  static VERSION = "1.0.0";
}

const appConfig = new Config();
console.log(appConfig.API_KEY); // super-secret-key
console.log(Config.VERSION); // 1.0.0

try {
  appConfig.API_KEY = "new-key"; // Ini akan gagal di strict mode atau diabaikan
} catch (e) {
  console.error(e.message); // TypeError: Cannot assign to read only property 'API_KEY'
}

try {
  Config.VERSION = "2.0.0"; // Ini juga akan gagal
} catch (e) {
  console.error(e.message); // TypeError: Cannot assign to read only property 'VERSION'
}

Decorator readonly memastikan bahwa setelah properti diinisialisasi, nilainya tidak dapat diubah. Ini sangat berguna untuk konfigurasi yang tidak boleh diutak-atik saat runtime.

⚠️ Peringatan: Untuk properti, decorator menerima initializer sebagai value, bukan nilai properti itu sendiri. Ini karena properti belum diinisialisasi saat decorator dijalankan. Decorator akan mengembalikan fungsi yang akan digunakan untuk menginisialisasi properti.

6. Membangun Decorator Kustom Anda Sendiri: Praktik Terbaik dan Pertimbangan

Sekarang setelah kita melihat beberapa contoh, mari kita bahas bagaimana Anda bisa mulai membangun decorator kustom Anda sendiri dan beberapa praktik terbaik.

Struktur Fungsi Decorator

Fungsi decorator dasar memiliki tanda tangan (value, context) => .... value bisa berupa:

context adalah objek penting yang memberikan informasi:

// Struktur umum decorator
function myCustomDecorator(value, context) {
  // Lakukan sesuatu dengan value dan context
  if (context.kind === 'method') {
    // Modifikasi metode
    return function(...args) {
      console.log(`Sebelum memanggil ${context.name}`);
      const result = value.apply(this, args);
      console.log(`Setelah memanggil ${context.name}`);
      return result;
    };
  }
  // Atau kembalikan nilai baru untuk elemen yang di-decorate
}

Decorator Factory

Seringkali Anda ingin decorator bisa menerima argumen (seperti debounce(500)). Untuk ini, Anda perlu membuat “decorator factory” – sebuah fungsi yang mengembalikan fungsi decorator.

function logWithPrefix(prefix) {
  return function(value, context) {
    if (context.kind === 'method') {
      return function(...args) {
        console.log(`${prefix} - Memanggil metode ${context.name}`);
        return value.apply(this, args);
      };
    }
  };
}

class ReportingService {
  @logWithPrefix("DEBUG")
  generateReport() {
    console.log("Laporan dibuat.");
  }
}

const service = new ReportingService();
service.generateReport(); // Output: DEBUG - Memanggil metode generateReport \n Laporan dibuat.

Tips dan Praktik Terbaik