Gagasan bahwa dalam pengembangan logika bisnis yang kurang lebih kompleks harus diberikan prioritas pada komposisi objek, daripada pewarisan yang populer di kalangan pengembang perangkat lunak dari berbagai jenis. Pada gelombang popularitas berikutnya dari paradigma pemrograman fungsional yang diluncurkan oleh keberhasilan ReactJS, pembicaraan tentang manfaat solusi komposisional datang ke ujung depan. Dalam posting ini ada sedikit tata letak di rak-rak teori komposisi objek dalam Javascript, contoh konkret, analisisnya dan jawaban atas pertanyaan berapa banyak keanggunan semantik yang harus dibayar pengguna (spoiler: banyak).
Vasily Kandinsky - "Komposisi X"Pengembangan sukses selama bertahun-tahun dari pendekatan berorientasi objek terhadap pembangunan, sebagian besar di bidang akademik, telah menyebabkan ketidakseimbangan yang nyata dalam pikiran pengembang rata-rata. Jadi, dalam banyak kasus, pemikiran pertama, jika perlu, untuk menggeneralisasi perilaku sejumlah entitas yang beragam adalah menciptakan kelas induk dan mewarisi perilaku ini. Pendekatan penyalahgunaan ini mengarah pada beberapa masalah yang menyulitkan desain dan menghambat pengembangan.
Pertama, kelas dasar yang dibebani dengan logika menjadi
rapuh - perubahan kecil dalam metodenya dapat berdampak fatal pada kelas turunan. Salah satu cara untuk mengatasi situasi ini adalah dengan mendistribusikan logika di beberapa kelas, menciptakan hierarki warisan yang lebih kompleks. Dalam hal ini, pengembang mendapatkan masalah lain - logika kelas induk digandakan dalam ahli waris sebagaimana diperlukan, dalam kasus persimpangan antara fungsionalitas kelas induk, tetapi, yang penting, tidak lengkap.
mail.mozilla.org/pipermail/es-discuss/2013-June/031614.htmlDan akhirnya, menciptakan hierarki yang cukup dalam, pengguna, ketika menggunakan entitas apa pun, dipaksa untuk menyeret semua leluhurnya beserta semua dependensi mereka, terlepas dari apakah ia akan menggunakan fungsionalitasnya atau tidak. Masalah ketergantungan berlebihan pada lingkungan dengan tangan ringan Joe Armstrong, pencipta Erlang, disebut masalah gorila dan pisang:
Saya pikir kurangnya usabilitas datang dalam bahasa berorientasi objek, bukan bahasa fungsional. Karena masalah dengan bahasa berorientasi objek adalah mereka memiliki semua lingkungan implisit yang mereka bawa. Anda menginginkan pisang, tetapi yang Anda dapatkan adalah gorila yang memegang pisang dan seluruh hutan.
Komposisi objek dipanggil untuk menyelesaikan semua masalah ini sebagai alternatif pewarisan kelas. Idenya sama sekali bukan hal baru, tetapi tidak menemukan pemahaman yang lengkap di antara pengembang. Situasi di dunia front-end sedikit lebih baik, di mana struktur proyek perangkat lunak seringkali cukup sederhana dan tidak merangsang penciptaan skema hubungan berorientasi objek yang kompleks. Namun, secara membabi buta mengikuti perjanjian Gang of Four, merekomendasikan bahwa komposisi lebih disukai daripada warisan, juga dapat memainkan trik pada kebijaksanaan inspirasional dari pengembang besar.
Mentransfer definisi dari Pola Desain ke dunia dinamis Javascript, kita dapat meringkas tiga jenis komposisi objek:
agregasi ,
gabungan, dan
delegasi . Patut dikatakan bahwa pemisahan ini dan konsep komposisi objek pada umumnya memiliki sifat yang murni teknis, sedangkan makna istilah-istilah ini memiliki persimpangan, yang menimbulkan kebingungan. Jadi, misalnya, kelas warisan dalam Javascript diimplementasikan berdasarkan delegasi (warisan prototipe). Oleh karena itu, setiap kasus lebih baik untuk dicadangkan dengan contoh kode langsung.
Agregasi adalah gabungan objek yang dapat dihitung, masing-masing dapat diperoleh menggunakan pengidentifikasi akses yang unik. Contohnya termasuk array, pohon, grafik. Contoh yang baik dari dunia pengembangan web adalah pohon DOM. Kualitas utama dari jenis komposisi ini dan alasan penciptaannya adalah kemampuan untuk dengan mudah menerapkan penangan kepada setiap anak dari komposisi.
Contoh sintetis adalah larik objek yang bergantian menetapkan gaya untuk elemen visual sewenang-wenang.
const styles = [ { fontSize: '12px', fontFamily: 'Arial' }, { fontFamily: 'Verdana', fontStyle: 'italic', fontWeight: 'bold' }, { fontFamily: 'Tahoma', fontStyle: 'normal'} ];
Setiap objek gaya dapat diekstraksi oleh indeksnya tanpa kehilangan informasi. Selain itu, menggunakan Array.prototype.map (), Anda dapat memproses semua nilai yang disimpan dengan cara yang diberikan.
const getFontFamily = s => s.fontFamily; styles.map(getFontFamily)
Penggabungan melibatkan perluasan fungsionalitas objek yang ada dengan menambahkan properti baru ke dalamnya. Jadi, misalnya, nyatakan reduksi dalam kerja Redux. Data yang diterima untuk memperbarui ditulis ke objek negara, memperluasnya. Data tentang keadaan objek saat ini, tidak seperti agregasi, akan hilang jika tidak disimpan.
Kembali ke contoh, secara bergantian menerapkan pengaturan di atas ke elemen visual, Anda dapat menghasilkan hasil akhir dengan menggabungkan parameter objek.
const concatenate = (a, s) => ({âĶa, âĶs}); styles.reduce(concatenate, {})
Nilai dari gaya yang lebih spesifik pada akhirnya akan menimpa status sebelumnya.
Saat
mendelegasikan , seperti yang Anda duga, satu objek didelegasikan ke yang lain. Delegasi, misalnya, adalah prototipe dalam Javascript. Contoh objek yang diturunkan mengarahkan panggilan ke metode induk. Jika tidak ada properti atau metode yang diperlukan dalam instance array, itu akan mengarahkan panggilan ini ke Array.prototype, dan jika perlu, lanjut ke Object.prototype. Dengan demikian, mekanisme pewarisan dalam Javascript didasarkan pada rantai delegasi prototipe, yang secara teknis versi (kejutan) dari komposisi.
Menggabungkan berbagai objek gaya dengan delegasi dapat dilakukan sebagai berikut.
const delegate = (a, b) => Object.assign(Object.create(a), b); styles.reduceRight(delegate, {})
Seperti yang Anda lihat, properti delegasi tidak dapat diakses oleh enumerasi (misalnya, menggunakan Object.keys ()), tetapi hanya dapat diakses dengan akses eksplisit. Fakta bahwa ini memberi kita adalah di akhir posting.
Sekarang untuk spesifik. Contoh kasus yang mendorong pengembang untuk menggunakan komposisi alih-alih pewarisan adalah dalam
Komposisi Objek Michael Rise
dalam Javascript . Di sini, penulis mempertimbangkan proses menciptakan hierarki karakter bermain peran. Awalnya, diperlukan dua jenis karakter - seorang prajurit dan pesulap, yang masing-masing memiliki cadangan kesehatan tertentu dan memiliki nama. Properti ini adalah umum dan dapat dipindahkan ke Karakter kelas induk.
class Character { constructor(name) { this.name = name; this.health = 100; } }
Prajurit dibedakan oleh fakta bahwa ia tahu cara menyerang, sambil menghabiskan staminanya, dan penyihir - kemampuan untuk merapal mantra, mengurangi jumlah mana.
class Fighter extends Character { constructor(name) { super(name); this.stamina = 100; } fight() { console.log(`${this.name} takes a mighty swing!`); this.stamina - ; } } class Mage extends Character { constructor(name) { super(name); this.mana = 100; } cast() { console.log(`${this.name} casts a fireball!`); this.mana - ; } }
Setelah menciptakan kelas Fighter dan Mage, keturunan Karakter, pengembang dihadapkan dengan masalah yang tidak terduga ketika ada kebutuhan untuk membuat kelas Paladin. Karakter baru dibedakan oleh kemampuan iri untuk bertarung dan menyulap. Begitu saja Anda dapat melihat beberapa solusi yang berbeda dalam kurangnya kasih karunia yang sama.
- Anda dapat menjadikan Paladin keturunan Karakter dan mengimplementasikan keduanya fight () dan cast () di dalamnya dari awal. Dalam kasus ini, prinsip KERING sangat dilanggar, karena masing-masing metode akan diduplikasi selama pembuatan dan selanjutnya akan membutuhkan sinkronisasi konstan dengan metode kelas Mage dan Fighter untuk melacak perubahan.
- Metode fight () dan cast () dapat diimplementasikan pada level kelas Charater sehingga ketiga tipe karakter memilikinya. Ini adalah solusi yang sedikit lebih menyenangkan, namun, dalam hal ini, pengembang harus mendefinisikan kembali metode pertarungan () untuk penyihir dan metode pemain () untuk prajurit, menggantinya dengan bertopik kosong.
Dalam salah satu opsi, cepat atau lambat seseorang harus menghadapi masalah warisan yang disuarakan di awal posting. Mereka dapat diselesaikan dengan pendekatan fungsional untuk implementasi karakter. Cukup untuk mendorong bukan dari tipenya, tetapi dari fungsinya. Pada intinya, kami memiliki dua fitur utama yang menentukan kemampuan karakter - kemampuan untuk bertarung dan kemampuan untuk menyulap. Fitur-fitur ini dapat diatur menggunakan fungsi pabrik yang memperluas status yang mendefinisikan karakter (contoh komposisi adalah penggabungan).
const canCast = (state) => ({ cast: (spell) => { console.log(`${state.name} casts ${spell}!`); state.mana - ; } }) const canFight = (state) => ({ fight: () => { console.log(`${state.name} slashes at the foe!`); state.stamina - ; } })
Dengan demikian, karakter ditentukan oleh himpunan fitur-fitur ini, dan properti awal, baik umum (nama dan kesehatan), dan pribadi (stamina dan mana).
const fighter = (name) => { let state = { name, health: 100, stamina: 100 } return Object.assign(state, canFight(state)); } const mage = (name) => { let state = { name, health: 100, mana: 100 } return Object.assign(state, canCast(state)); } const paladin = (name) => { let state = { name, health: 100, mana: 100, stamina: 100 } return Object.assign(state, canCast(state), canFight(state)); }
Semuanya indah - kode tindakan digunakan kembali, Anda dapat menambahkan karakter baru dengan mudah, tanpa menyentuh yang sebelumnya dan tanpa menggembungkan fungsionalitas objek apa pun. Untuk menemukan lalat dalam salep dalam solusi yang diusulkan, cukup membandingkan kinerja solusi berdasarkan pewarisan (baca delegasi) dan solusi berdasarkan gabungan. Buat sepersejuta juta instance dari karakter yang dibuat.
var inheritanceArmy = []; for (var i = 0; i < 1000000; i++) { inheritanceArmy.push(new Fighter('Fighter' + i)); inheritanceArmy.push(new Mage('Mage' + i)); } var compositionArmy = []; for (var i = 0; i < 1000000; i++) { compositionArmy.push(fighter('Fighter' + i)); compositionArmy.push(mage('Mage' + i)); }
Dan membandingkan memori dan biaya komputasi antara warisan dan komposisi yang diperlukan untuk membuat objek.

Rata-rata, solusi menggunakan komposisi dengan penggabungan membutuhkan 100â150% lebih banyak sumber daya. Hasil yang disajikan diperoleh di lingkungan NodeJS, Anda dapat melihat hasil untuk mesin browser dengan menjalankan
tes ini.
Keuntungan dari solusi berdasarkan pewarisan-delegasi dapat dijelaskan dengan menghemat memori karena kurangnya akses implisit ke properti delegasi, serta menonaktifkan beberapa optimasi mesin untuk delegasi dinamis. Pada gilirannya, solusi berbasis gabungan menggunakan metode Object.assign () yang sangat mahal, yang sangat mempengaruhi kinerjanya. Menariknya, Firefox Quantum menunjukkan hasil Chromium yang berlawanan secara berlawanan - solusi kedua bekerja lebih cepat di Gecko.
Tentu saja, sangat berguna untuk mengandalkan hasil tes kinerja hanya ketika menyelesaikan tugas-tugas yang agak melelahkan terkait dengan menciptakan sejumlah besar objek infrastruktur yang kompleks - misalnya, ketika bekerja dengan pohon elemen virtual atau mengembangkan perpustakaan grafis. Dalam kebanyakan kasus, keindahan struktural dari suatu solusi, keandalan dan kesederhanaannya ternyata lebih penting, dan perbedaan kecil dalam kinerja tidak memainkan peran besar (operasi dengan elemen DOM akan membutuhkan lebih banyak sumber daya).
Kesimpulannya, perlu dicatat bahwa jenis komposisi yang dipertimbangkan tidak unik dan saling eksklusif. Delegasi dapat diimplementasikan menggunakan agregasi, dan pewarisan kelas menggunakan delegasi (seperti yang dilakukan dalam JavaScript). Pada intinya, setiap kombinasi objek akan menjadi satu atau lain bentuk komposisi, dan pada akhirnya hanya kesederhanaan dan fleksibilitas dari solusi yang dihasilkan yang penting.