Belajar dengan membaca secara berurutan

Composition vs Inheritance — Object.assign dan Mixin

Fitur dipecah menjadi komponen, lalu digabungkan ke satu object. Artikel ini menjelaskan Object.assign, function yang mengembalikan komponen, mixin function, dan kapan sebaiknya memakai inheritance.

Di sistem dokumen kantormu, notulen rapat perlu fitur ekspor PDF, kontrak perlu fitur persetujuan, dan penawaran harga perlu keduanya. Di belakang extends, sebuah class hanya bisa menyebut satu parent, sehingga kamu terpaksa menulis satu class untuk setiap kombinasi fitur.

Artikel ini membahas composition, yang memecah fitur menjadi komponen terpisah lalu menambahkannya ke object, dan mixin function, yang menumpuk fitur ke sebuah class.

Menambahkan Hanya Komponen yang Dibutuhkan Setiap Dokumen — Object.assign

Misalkan kamu sudah membuat class ApprovableDocument yang mendukung persetujuan. Karena penawaran harga juga butuh ekspor PDF, kamu membuat class ExportableDocument yang meng-extend-nya. Notulen rapat hanya butuh ekspor PDF, jadi notulen terpaksa ikut mewarisi fitur persetujuan yang tidak pernah dipakai, atau meng-extend Document sendiri lalu menyalin kode PDF-nya.

Dengan composition (menyusun object dari beberapa kelompok method yang terpisah, satu kelompok per fitur), kamu memberikan komponen-komponen itu ke Object.assign(target, komponen1, komponen2). Function ini menyalin property dari argument kedua dan seterusnya ke argument pertama, lalu mengembalikan argument pertama itu.

class Document {
  constructor(title) { this.title = title; }
}

// Buat satu kelompok method (komponen) untuk setiap fitur
const approvable = {
  approve(name) { return `${this.title}: disetujui oleh ${name}`; },
};
const exportable = {
  toPdfName() { return `${this.title}.pdf`; },
};

// Salin hanya komponen yang dibutuhkan setiap dokumen
const minutes = Object.assign(new Document("Notulen rapat mingguan"), exportable);
const estimate = Object.assign(new Document("Penawaran servis"), approvable, exportable);

console.log(Object.keys(estimate).join(", "));   // title, approve, toPdfName
console.log(estimate.approve("Budi"));          // Penawaran servis: disetujui oleh Budi
console.log(minutes.toPdfName());               // Notulen rapat mingguan.pdf
console.log(typeof minutes.approve);            // undefined (notulen tidak punya fitur persetujuan)
Urutan Penyalinan Object.assign
new Document("Penawaranservis")Saat ini key-nyahanya titleMenyalin approvedari approvableapprovable sendiritidak berubahMenyalin toPdfNamedari exportableMengembalikaninstance targetyang sama
Property setiap komponen disalin ke instance yang sama, dari kiri ke kanan. Tidak ada object baru yang dibuat; return value-nya adalah target itu sendiri.

Pada estimate.approve("Budi"), object di sebelah kiri titik adalah estimate, jadi this di dalam approve adalah estimate, dan this.title adalah judul penawaran itu. Target-nya tetap instance hasil new Document, jadi instanceof Document juga bernilai true.

Di aplikasi chat tim, kamu akan menambahkan fitur sematkan (pin) ke pesan yang masuk. messages dan pinnable sudah dideklarasikan.

① Salin pinnable ke pesan pertama, lalu simpan return value-nya di sebuah variabel.

② Sematkan pesan lewat variabel dari ①, lalu tampilkan return value-nya dan property pinned milik pesan pertama.

③ Tampilkan apakah return value dari ① adalah object yang sama dengan pesan pertama.

④ Tampilkan tipe pin milik pesan kedua.

(Kalau berjalan dengan benar, penjelasan akan muncul.)

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

Daftar Komentar Terpisah untuk Setiap Dokumen — Function yang Mengembalikan Komponen

Sekarang kamu juga ingin daftar komentar, jadi kamu menjadikan { comments: [], addComment(text) { ... } } sebagai komponen dan menyalinnya ke notulen dan penawaran dengan Object.assign. Yang disalin hanya reference ke array-nya, jadi comments di kedua dokumen menunjuk ke array yang sama, dan komentar yang ditambahkan ke notulen juga muncul di daftar komentar penawaran.

Kalau komponen komentar kamu tulis ulang sebagai commentable, yaitu function yang membuat dan mengembalikan komponen baru setiap kali dipanggil, comments mendapat array [] baru di setiap pemanggilan. Jadikan approvable function juga, lalu tulis { ...approvable(), ...commentable() }: spread syntax menyalin key dan value setiap komponen ke object baru.

// Function yang mengembalikan komponen: setiap pemanggilan membuat object dan array baru
const commentable = () => ({
  comments: [],
  addComment(text) { this.comments.push(text); },
});
const approvable = () => ({
  approvedBy: "belum disetujui",
  approve(name) { this.approvedBy = name; },
});

// Susun komponen dengan spread syntax untuk membuat dokumen baru
const minutes = { title: "Notulen rapat mingguan", ...commentable() };
const estimate = { title: "Penawaran servis", ...approvable(), ...commentable() };

minutes.addComment("Lanjut tgl 10");
estimate.approve("Budi");
console.log(minutes.comments.length, estimate.comments.length);   // 1 0 (array-nya terpisah)
console.log(estimate.approvedBy);                                  // Budi
console.log(Object.keys(estimate).join(", "));   // title, approvedBy, approve, comments, addComment
Apakah Komentar di Notulen Muncul di Penawaran?
Object.assignkomponen yang samake keduanyacomments merujukke array yang samaminutes.addComment("Lanjut tgl 10")estimate.commentsikut berisi 1Panggilcommentable()per dokumencomments berisi[] baru setiapdipanggilminutes.addComment("Lanjut tgl 10")estimate.commentstetap 0
Satu komentar ditambahkan ke notulen, tetapi komentar itu ikut muncul di penawaran hanya pada baris atas. Yang disalin adalah reference ke array, jadi buat ulang komponennya untuk setiap dokumen.

Yang membuat object baru adalah { }; spread syntax hanya menyalin key dari return value setiap komponen ke dalamnya. Berbeda dengan Object.assign, kamu tidak menyiapkan target lebih dulu, jadi hasilnya adalah object biasa, bukan instance dari sebuah class.

Kalau Ada Key yang Sama, Komponen Terakhir yang Dipakai

Baik dengan Object.assign maupun spread syntax, kalau dua komponen punya nama key yang sama, yang tersisa adalah value dari komponen yang diberikan paling akhir. Kalau approvable() dan commentable() sama-sama punya describe, hanya yang terakhir yang dipakai dan tidak ada error yang muncul, jadi pastikan nama method tidak bentrok antarkomponen.

Di aplikasi chat tim, kamu akan membuat pesan yang punya reaksi dan tanda sudah dibaca. reactable sudah dideklarasikan.

① Definisikan function yang mengembalikan komponen baru setiap kali dipanggil. Komponen itu menambahkan nama setiap orang yang sudah membaca pesan ke sebuah array. Tambahkan setiap nama hanya sekali.

② Gabungkan kedua komponen untuk membuat pesan dari Budi dan dari Siti.

③ Beri reaksi ke pesan Budi dan tandai sudah dibaca tiga kali, dengan satu orang membacanya dua kali.

④ Tampilkan jumlah reaksi dan jumlah pembaca untuk kedua pesan.

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

Menumpuk Fitur di Definisi Class — Mixin Function

Di bagian lain sistem, penawaran didefinisikan sebagai class Estimate dan dibuat dengan new Estimate(...) di setiap layar. Kalau fitur persetujuan dan komentar kamu tambahkan belakangan dengan Object.assign, kamu harus menyalinnya setelah setiap new, dan penawaran yang terlewat akan melempar TypeError saat approve dipanggil.

Mixin function (function yang menerima class dan mengembalikan class yang meng-extend class itu dengan fitur tambahan) ditulis sebagai (Base) => class extends Base { ... }. class extends Base { ... } adalah class expression tanpa nama. Kamu bisa menulis pemanggilan function ini setelah extends, dan class yang dikembalikannya menjadi parent.

class Document {
  constructor(title) { this.title = title; }
  describe() { return this.title; }
}

// Menerima class dan mengembalikan class yang meng-extend-nya dengan fitur tambahan
const Approvable = (Base) => class extends Base {
  approve(name) { this.approvedBy = name; }
  describe() { return `${super.describe()} (${this.approvedBy ?? "belum disetujui"})`; }
};
const Commentable = (Base) => class extends Base {
  comments = [];                                  // Array baru untuk setiap instance
  addComment(text) { this.comments.push(text); }
};

// Keduanya ditumpuk di definisi class, jadi tidak ada yang perlu disalin setelah setiap new
class Estimate extends Commentable(Approvable(Document)) {}
const estimate = new Estimate("Penawaran servis");
estimate.approve("Budi");
console.log(estimate.describe());   // Penawaran servis (Budi)
Dua Class yang Ditumpuk di Atas Estimate
class Estimatedengan isi kosongPencarian describedimulai di siniClass dariCommentablecomments danaddCommentClass dariApprovableapprove dandescribeditemukan di siniclass Documentsuper.describe()→ "Penawaranservis"
Approvable, yang ditulis di bagian dalam, berada lebih dekat ke Document. Walaupun Estimate kosong, pencarian method menelusuri keempat class secara berurutan.

Di Approvable, super.describe() memanggil describe milik Document, yaitu Base yang diterimanya. Kalau urutan tumpukannya kamu tukar menjadi Approvable(Commentable(Document)), Estimate tetap mendapat fitur persetujuan dan komentar.

Meng-extend Mixin Tanpa Memanggilnya Melempar TypeError

extends Approvable menjadikan arrow function itu sendiri sebagai parent. Arrow function tidak bisa menjadi parent sebuah class, jadi JavaScript melempar TypeError: Class extends value ... is not a constructor or null. Tulis Approvable(Document) sebagai parent-nya.

Membandingkan Dua Pendekatan — Mixin vs extends

Kalau kamu menulis mixin function untuk setiap fitur, tanda kurungnya mulai bersarang, seperti class Estimate extends Commentable(Approvable(Document)) {}. Makin banyak yang ditumpuk, makin sulit menangkap dari satu baris deklarasi itu apa sebenarnya Estimate dan fitur apa saja yang ditambahkan padanya.

Bandingkan dengan versi inheritance: CommentableDocument (komentar) meng-extend ApprovableDocument (persetujuan). Dengan kedua pendekatan, instanceof Document bernilai true dan kamu bisa memakai super. Bedanya ada pada apakah kamu bisa memilih fitur untuk setiap class.

class Document { constructor(title) { this.title = title; } }
const Commentable = (Base) => class extends Base {   // Mixin yang sama dengan bagian sebelumnya
  comments = []; addComment(text) { this.comments.push(text); }
};

// Inheritance: class komentar dibangun di atas class persetujuan
class ApprovableDocument extends Document {
  approve(name) { this.approvedBy = name; }
}
class CommentableDocument extends ApprovableDocument {
  comments = [];
  addComment(text) { this.comments.push(text); }
}
const oldMinutes = new CommentableDocument("Notulen rapat mingguan");
console.log(typeof oldMinutes.approve);    // function (ikut mewarisi persetujuan yang tidak dipakai)

// Menumpuk dengan mixin
class Minutes extends Commentable(Document) {}
const minutes = new Minutes("Notulen rapat mingguan");
console.log(typeof minutes.approve);       // undefined (hanya fitur komentar yang ditambahkan)
console.log(minutes instanceof Document, oldMinutes instanceof Document);   // true true
Fitur Notulen dengan Inheritance vs Composition
oldMinutes adalahCommentable-DocumentParent-nyaApprovable-Documentapprove ditemukandi Approvable-Documenttypeof approveadalah functionminutes adalahMinutesParent-nya classCommentableTidak ada approvesampai Documenttypeof approveadalah undefined
Dengan inheritance, notulen bahkan mendapat approve dari parent-nya, ApprovableDocument. Dengan mixin, notulen hanya mendapat fitur Commentable.

Fitur yang dibutuhkan sebagian jenis object tetapi tidak dibutuhkan yang lain, seperti persetujuan dan komentar, sebaiknya dijadikan komponen. Pakai extends untuk hubungan yang cocok dengan satu garis parent-child, misalnya "penawaran adalah salah satu jenis dokumen". Tabel di bawah merangkum empat pendekatan yang dipakai di artikel ini.

PendekatanCara fitur ditambahkanCocok untuk
extends ParentWarisi semua dari parentHubungan "jenis dari"
Object.assign(target, komponen)Salin ke object yang adaTambah fitur ke 1 object
{ ...komponen() }Salin ke object baruSusun tanpa class
extends Mixin(Parent)Tumpuk fitur pilihanTambah di definisi class

Di aplikasi chat tim, kamu akan membuat tiga jenis pesan: hanya bisa disematkan, hanya bisa diberi reaksi, dan bisa keduanya. Message dan Reactable sudah dideklarasikan.

① Definisikan mixin function yang mengembalikan class dengan fitur sematkan.

② Definisikan ketiga class.

③ Buat satu pesan untuk setiap jenis, lalu tampilkan tipe pin dan react.

④ Beri reaksi ke pesan yang punya kedua fitur, lalu tampilkan hasil pin dan jumlah reaksinya.

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output
QUIZ

Cek Pemahaman

Jawab setiap pertanyaan satu per satu.

Soal 1Apa hasil instanceof Document untuk minutes, yang komponennya disalin dengan Object.assign?

Soal 2Kalau comments: [] dari satu komponen kamu salin ke dua dokumen dengan Object.assign, lalu kamu push ke salah satunya, apa yang terjadi?

Soal 3Pada Commentable(Approvable(Document)), class mana yang langsung meng-extend Document?