Panduan Komprehensif untuk Pola Desain JavaScript
Diterbitkan: 2022-03-11Sebagai pengembang JavaScript yang baik, Anda berusaha untuk menulis kode yang bersih, sehat, dan dapat dipelihara. Anda memecahkan tantangan menarik yang, meski unik, tidak selalu membutuhkan solusi unik. Anda mungkin menemukan diri Anda menulis kode yang terlihat mirip dengan solusi dari masalah yang sama sekali berbeda yang pernah Anda tangani sebelumnya. Anda mungkin tidak mengetahuinya, tetapi Anda telah menggunakan pola desain JavaScript . Pola desain adalah solusi yang dapat digunakan kembali untuk masalah yang sering terjadi dalam desain perangkat lunak.
Selama masa pakai bahasa apa pun, banyak solusi yang dapat digunakan kembali seperti itu dibuat dan diuji oleh sejumlah besar pengembang dari komunitas bahasa itu. Karena pengalaman gabungan dari banyak pengembang inilah solusi semacam itu sangat berguna karena membantu kami menulis kode dengan cara yang dioptimalkan sambil pada saat yang sama memecahkan masalah yang dihadapi.
Manfaat utama yang kami dapatkan dari pola desain adalah sebagai berikut:
- Mereka adalah solusi yang terbukti: Karena pola desain sering digunakan oleh banyak pengembang, Anda dapat yakin bahwa pola tersebut berfungsi. Dan tidak hanya itu, Anda dapat yakin bahwa itu direvisi beberapa kali dan pengoptimalan mungkin diterapkan.
- Mereka mudah digunakan kembali: Pola desain mendokumentasikan solusi yang dapat digunakan kembali yang dapat dimodifikasi untuk memecahkan beberapa masalah tertentu, karena tidak terikat pada masalah tertentu.
- Mereka ekspresif: Pola desain dapat menjelaskan solusi besar dengan cukup elegan.
- Mereka memudahkan komunikasi: Ketika pengembang terbiasa dengan pola desain, mereka dapat lebih mudah berkomunikasi satu sama lain tentang solusi potensial untuk masalah yang diberikan.
- Mereka mencegah perlunya refactoring kode: Jika aplikasi ditulis dengan pola desain dalam pikiran, sering kali Anda tidak perlu refactor kode nanti karena menerapkan pola desain yang benar untuk masalah yang diberikan sudah optimal larutan.
- Mereka menurunkan ukuran basis kode: Karena pola desain biasanya merupakan solusi yang elegan dan optimal, mereka biasanya membutuhkan lebih sedikit kode daripada solusi lain.
Saya tahu Anda siap untuk terjun pada saat ini, tetapi sebelum Anda mempelajari semua tentang pola desain, mari kita tinjau beberapa dasar JavaScript.
Sejarah Singkat JavaScript
JavaScript adalah salah satu bahasa pemrograman paling populer untuk pengembangan web saat ini. Awalnya dibuat sebagai semacam "lem" untuk berbagai elemen HTML yang ditampilkan, yang dikenal sebagai bahasa skrip sisi klien, untuk salah satu browser web awal. Disebut Netscape Navigator, itu hanya bisa menampilkan HTML statis pada saat itu. Seperti yang Anda duga, gagasan bahasa scripting seperti itu menyebabkan perang browser antara pemain besar di industri pengembangan browser saat itu, seperti Netscape Communications (sekarang Mozilla), Microsoft, dan lainnya.
Setiap pemain besar ingin mendorong implementasi bahasa scripting mereka sendiri, jadi Netscape membuat JavaScript (sebenarnya, Brendan Eich melakukannya), Microsoft membuat JScript, dan sebagainya. Seperti yang dapat Anda bayangkan, perbedaan antara implementasi ini sangat bagus, jadi pengembangan untuk browser web dilakukan per browser, dengan stiker dengan tampilan terbaik yang disertakan dengan halaman web. Segera menjadi jelas bahwa kami membutuhkan standar, solusi lintas-browser yang akan menyatukan proses pengembangan dan menyederhanakan pembuatan halaman web. Apa yang mereka hasilkan disebut ECMAScript.
ECMAScript adalah spesifikasi bahasa skrip standar yang coba didukung oleh semua browser modern, dan ada beberapa implementasi (bisa dibilang dialek) dari ECMAScript. Yang paling populer adalah topik artikel ini, JavaScript. Sejak rilis awal, ECMAScript telah menstandarisasi banyak hal penting, dan bagi mereka yang lebih tertarik pada hal-hal spesifik, ada daftar rinci item standar untuk setiap versi ECMAScript yang tersedia di Wikipedia. Dukungan browser untuk ECMAScript versi 6 (ES6) dan yang lebih tinggi masih belum lengkap dan harus ditranskripsikan ke ES5 agar dapat didukung sepenuhnya.
Apa itu JavaScript?
Untuk memahami sepenuhnya isi artikel ini, mari kita memperkenalkan beberapa karakteristik bahasa yang sangat penting yang perlu kita ketahui sebelum menyelami pola desain JavaScript. Jika seseorang bertanya kepada Anda "Apa itu JavaScript?" Anda mungkin menjawab di suatu tempat di baris:
JavaScript adalah bahasa pemrograman berorientasi objek yang ringan, ditafsirkan, dengan fungsi kelas satu yang paling umum dikenal sebagai bahasa skrip untuk halaman web.
Definisi di atas berarti mengatakan bahwa kode JavaScript memiliki jejak memori yang rendah, mudah diterapkan, dan mudah dipelajari, dengan sintaks yang mirip dengan bahasa populer seperti C++ dan Java. Ini adalah bahasa skrip, yang berarti bahwa kodenya ditafsirkan alih-alih dikompilasi. Ini memiliki dukungan untuk gaya pemrograman prosedural, berorientasi objek, dan fungsional, yang membuatnya sangat fleksibel untuk pengembang.
Sejauh ini, kita telah melihat semua karakteristik yang terdengar seperti banyak bahasa lain di luar sana, jadi mari kita lihat apa yang spesifik tentang JavaScript dalam kaitannya dengan bahasa lain. Saya akan membuat daftar beberapa karakteristik dan memberikan kesempatan terbaik saya untuk menjelaskan mengapa mereka layak mendapat perhatian khusus.
JavaScript Mendukung Fungsi Kelas Satu
Karakteristik ini dulunya sulit untuk saya pahami ketika saya baru memulai dengan JavaScript, karena saya berasal dari latar belakang C/C++. JavaScript memperlakukan fungsi sebagai warga kelas satu, artinya Anda dapat meneruskan fungsi sebagai parameter ke fungsi lain seperti halnya variabel lainnya.
// we send in the function as an argument to be // executed from inside the calling function function performOperation(a, b, cb) { var c = a + b; cb(c); } performOperation(2, 3, function(result) { // prints out 5 console.log("The result of the operation is " + result); })
JavaScript Berbasis Prototipe
Seperti halnya dengan banyak bahasa berorientasi objek lainnya, JavaScript mendukung objek, dan salah satu istilah pertama yang muncul di benak ketika memikirkan objek adalah kelas dan pewarisan. Di sinilah menjadi sedikit rumit, karena bahasa tidak mendukung kelas dalam bentuk bahasa sederhana melainkan menggunakan sesuatu yang disebut pewarisan berbasis prototipe atau berbasis instance.
Baru saja, di ES6, kelas istilah formal diperkenalkan, yang berarti bahwa browser masih tidak mendukung ini (jika Anda ingat, pada saat penulisan, versi ECMAScript terakhir yang didukung penuh adalah 5.1). Penting untuk dicatat, bagaimanapun, bahwa meskipun istilah "kelas" diperkenalkan ke dalam JavaScript, masih menggunakan pewarisan berbasis prototipe di bawah tenda.
Pemrograman berbasis prototipe adalah gaya pemrograman berorientasi objek di mana penggunaan kembali perilaku (dikenal sebagai pewarisan) dilakukan melalui proses penggunaan kembali objek yang ada melalui delegasi yang berfungsi sebagai prototipe. Kami akan menyelami lebih detail dengan ini setelah kami sampai ke bagian pola desain artikel, karena karakteristik ini digunakan dalam banyak pola desain JavaScript.
Loop Peristiwa JavaScript
Jika Anda memiliki pengalaman bekerja dengan JavaScript, Anda pasti akrab dengan istilah fungsi panggilan balik . Bagi mereka yang tidak terbiasa dengan istilah tersebut, fungsi panggilan balik adalah fungsi yang dikirim sebagai parameter (ingat, JavaScript memperlakukan fungsi sebagai warga kelas satu) ke fungsi lain dan dieksekusi setelah suatu peristiwa diaktifkan. Ini biasanya digunakan untuk berlangganan acara seperti klik mouse atau tekan tombol keyboard.
Setiap kali suatu peristiwa, yang memiliki pendengar yang melekat padanya, kebakaran (jika acara tersebut hilang), sebuah pesan sedang dikirim ke antrian pesan yang sedang diproses secara sinkron, dengan cara FIFO (pertama-masuk-pertama-keluar ). Ini disebut loop acara .
Setiap pesan pada antrian memiliki fungsi yang terkait dengannya. Setelah pesan di-dequeued, runtime menjalankan fungsi sepenuhnya sebelum memproses pesan lainnya. Artinya, jika suatu fungsi berisi panggilan fungsi lain, semuanya dilakukan sebelum memproses pesan baru dari antrian. Ini disebut run-to-completion.
while (queue.waitForMessage()) { queue.processNextMessage(); }
queue.waitForMessage()
secara sinkron menunggu pesan baru. Setiap pesan yang sedang diproses memiliki tumpukannya sendiri dan diproses hingga tumpukan tersebut kosong. Setelah selesai, pesan baru diproses dari antrian, jika ada.
Anda mungkin juga pernah mendengar bahwa JavaScript non-blocking, artinya ketika operasi asinkron sedang dilakukan, program dapat memproses hal-hal lain, seperti menerima input pengguna, sambil menunggu operasi asinkron selesai, tidak memblokir yang utama. benang eksekusi. Ini adalah properti JavaScript yang sangat berguna dan seluruh artikel dapat ditulis hanya tentang topik ini; namun, itu di luar cakupan artikel ini.
Apa itu Pola Desain?
Seperti yang saya katakan sebelumnya, pola desain adalah solusi yang dapat digunakan kembali untuk masalah yang sering terjadi dalam desain perangkat lunak. Mari kita lihat beberapa kategori pola desain.
Pola-proto
Bagaimana cara membuat pola? Katakanlah Anda mengenali masalah yang umum terjadi, dan Anda memiliki solusi unik Anda sendiri untuk masalah ini, yang tidak diakui dan didokumentasikan secara global. Anda menggunakan solusi ini setiap kali Anda menghadapi masalah ini, dan menurut Anda solusi ini dapat digunakan kembali dan komunitas pengembang dapat mengambil manfaat darinya.
Apakah itu segera menjadi pola? Untungnya, tidak. Seringkali, seseorang mungkin memiliki praktik penulisan kode yang baik dan hanya salah mengira sesuatu yang terlihat seperti pola padahal sebenarnya itu bukan pola.
Bagaimana Anda bisa tahu kapan apa yang Anda pikir Anda kenali sebenarnya adalah pola desain?
Dengan mendapatkan pendapat pengembang lain tentang hal itu, dengan mengetahui tentang proses pembuatan pola itu sendiri, dan dengan membiasakan diri dengan pola yang ada. Ada fase yang harus dilalui sebuah pola sebelum menjadi pola yang utuh, dan ini disebut pola proto.
Pola-proto adalah calon pola jika melewati periode pengujian tertentu oleh berbagai pengembang dan skenario di mana pola tersebut terbukti berguna dan memberikan hasil yang benar. Ada cukup banyak pekerjaan dan dokumentasi—yang sebagian besar berada di luar cakupan artikel ini—yang harus dilakukan agar pola yang lengkap diakui oleh masyarakat.
Anti-pola
Karena pola desain mewakili praktik yang baik, anti-pola mewakili praktik yang buruk.
Contoh anti-pola akan memodifikasi prototipe kelas Object
. Hampir semua objek dalam JavaScript mewarisi dari Object
(ingat bahwa JavaScript menggunakan pewarisan berbasis prototipe) jadi bayangkan skenario di mana Anda mengubah prototipe ini. Perubahan pada prototipe Object
akan terlihat di semua objek yang diwarisi dari prototipe ini— yang akan menjadi sebagian besar objek JavaScript . Ini adalah bencana yang menunggu untuk terjadi.
Contoh lain, mirip dengan yang disebutkan di atas, adalah memodifikasi objek yang bukan milik Anda. Contohnya adalah mengganti fungsi dari objek yang digunakan dalam banyak skenario di seluruh aplikasi. Jika Anda bekerja dengan tim besar, bayangkan kebingungan yang akan ditimbulkannya; Anda akan segera mengalami tabrakan penamaan, implementasi yang tidak kompatibel, dan mimpi buruk pemeliharaan.
Serupa dengan betapa bermanfaatnya mengetahui semua praktik dan solusi yang baik, juga sangat penting untuk mengetahui yang buruk juga. Dengan cara ini, Anda dapat mengenali mereka dan menghindari membuat kesalahan di depan.
Kategorisasi Pola Desain
Pola desain dapat dikategorikan dalam beberapa cara, tetapi yang paling populer adalah sebagai berikut:
- Pola desain kreasi
- Pola desain struktural
- Pola desain perilaku
- Pola desain konkurensi
- Pola desain arsitektur
Pola Desain Kreasi
Pola-pola ini berhubungan dengan mekanisme pembuatan objek yang mengoptimalkan pembuatan objek dibandingkan dengan pendekatan dasar. Bentuk dasar pembuatan objek dapat mengakibatkan masalah desain atau menambah kompleksitas desain. Pola desain kreasi memecahkan masalah ini dengan mengontrol pembuatan objek. Beberapa pola desain yang populer dalam kategori ini adalah:
- Metode pabrik
- Pabrik abstrak
- Pembangun
- Prototipe
- lajang
Pola Desain Struktural
Pola-pola ini berhubungan dengan hubungan objek. Mereka memastikan bahwa jika satu bagian dari suatu sistem berubah, seluruh sistem tidak perlu ikut berubah. Pola paling populer dalam kategori ini adalah:
- Adaptor
- Menjembatani
- Gabungan
- Penghias
- Tatapan
- Kelas terbang
- Proksi
Pola Desain Perilaku
Jenis pola ini mengenali, menerapkan, dan meningkatkan komunikasi antara objek yang berbeda dalam suatu sistem. Mereka membantu memastikan bahwa bagian yang berbeda dari suatu sistem telah menyinkronkan informasi. Contoh populer dari pola ini adalah:
- Rantai tanggung jawab
- Memerintah
- Pengulangan
- Penengah
- kenang-kenangan
- Pengamat
- Negara
- Strategi
- Pengunjung
Pola Desain Konkurensi
Jenis pola desain ini berhubungan dengan paradigma pemrograman multi-utas. Beberapa yang populer adalah:
- Objek aktif
- Reaksi nuklir
- Penjadwal
Pola Desain Arsitektur
Pola desain yang digunakan untuk tujuan arsitektur. Beberapa yang paling terkenal adalah:
- MVC (Model-View-Controller)
- MVP (Model-View-Presenter)
- MVVM (Model-View-ViewModel)
Pada bagian berikut, kita akan melihat lebih dekat beberapa pola desain yang disebutkan di atas dengan contoh yang diberikan untuk pemahaman yang lebih baik.
Contoh Pola Desain
Masing-masing pola desain mewakili jenis solusi tertentu untuk jenis masalah tertentu. Tidak ada set pola universal yang selalu paling cocok. Kita perlu belajar kapan pola tertentu akan terbukti berguna dan apakah itu akan memberikan nilai aktual. Setelah kita terbiasa dengan pola dan skenario yang paling cocok untuknya, kita dapat dengan mudah menentukan apakah pola tertentu cocok untuk masalah tertentu atau tidak.
Ingat, menerapkan pola yang salah untuk masalah tertentu dapat menyebabkan efek yang tidak diinginkan seperti kompleksitas kode yang tidak perlu, overhead yang tidak perlu pada kinerja, atau bahkan munculnya anti-pola baru.
Ini semua adalah hal penting untuk dipertimbangkan ketika berpikir tentang menerapkan pola desain ke kode kita. Kita akan melihat beberapa pola desain yang menurut saya pribadi berguna dan percaya bahwa setiap pengembang JavaScript senior harus mengenalnya.
Pola Konstruktor
Ketika berpikir tentang bahasa berorientasi objek klasik, konstruktor adalah fungsi khusus di kelas yang menginisialisasi objek dengan beberapa set nilai default dan/atau nilai terkirim.
Cara umum untuk membuat objek dalam JavaScript adalah tiga cara berikut:
// either of the following ways can be used to create a new object var instance = {}; // or var instance = Object.create(Object.prototype); // or var instance = new Object();
Setelah membuat objek, ada empat cara (sejak ES3) untuk menambahkan properti ke objek ini. Mereka adalah sebagai berikut:
// supported since ES3 // the dot notation instance.key = "A key's value"; // the square brackets notation instance["key"] = "A key's value"; // supported since ES5 // setting a single property using Object.defineProperty Object.defineProperty(instance, "key", { value: "A key's value", writable: true, enumerable: true, configurable: true }); // setting multiple properties using Object.defineProperties Object.defineProperties(instance, { "firstKey": { value: "First key's value", writable: true }, "secondKey": { value: "Second key's value", writable: false } });
Cara paling populer untuk membuat objek adalah kurung kurawal dan, untuk menambahkan properti, notasi titik atau kurung siku. Siapa pun yang memiliki pengalaman dengan JavaScript telah menggunakannya.
Kami menyebutkan sebelumnya bahwa JavaScript tidak mendukung kelas asli, tetapi mendukung konstruktor melalui penggunaan kata kunci "baru" yang diawali dengan panggilan fungsi. Dengan cara ini, kita dapat menggunakan fungsi sebagai konstruktor dan menginisialisasi propertinya dengan cara yang sama seperti yang kita lakukan dengan konstruktor bahasa klasik.
// we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; this.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();
Namun, masih ada ruang untuk perbaikan di sini. Jika Anda ingat, saya sebutkan sebelumnya bahwa JavaScript menggunakan pewarisan berbasis prototipe. Masalah dengan pendekatan sebelumnya adalah bahwa metode writesCode
didefinisikan ulang untuk setiap instance konstruktor Person
. Kita dapat menghindari ini dengan mengatur metode ke dalam prototipe fungsi:
// we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; } // we extend the function's prototype Person.prototype.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();
Sekarang, kedua instance konstruktor Person
dapat mengakses instance bersama dari metode writesCode()
.

Pola Modul
Sejauh kekhasan pergi, JavaScript tidak pernah berhenti memukau. Hal aneh lainnya pada JavaScript (setidaknya sejauh bahasa berorientasi objek berjalan) adalah bahwa JavaScript tidak mendukung pengubah akses. Dalam bahasa OOP klasik, pengguna mendefinisikan kelas dan menentukan hak akses untuk anggotanya. Karena JavaScript dalam bentuknya yang sederhana tidak mendukung kelas atau pengubah akses, pengembang JavaScript menemukan cara untuk meniru perilaku ini bila diperlukan.
Sebelum kita masuk ke spesifik pola modul, mari kita bicara tentang konsep penutupan. Penutupan adalah fungsi dengan akses ke lingkup induk, bahkan setelah fungsi induk ditutup. Mereka membantu kami meniru perilaku pengubah akses melalui pelingkupan. Mari kita tunjukkan ini melalui sebuah contoh:
// we used an immediately invoked function expression // to create a private variable, counter var counterIncrementer = (function() { var counter = 0; return function() { return ++counter; }; })(); // prints out 1 console.log(counterIncrementer()); // prints out 2 console.log(counterIncrementer()); // prints out 3 console.log(counterIncrementer());
Seperti yang Anda lihat, dengan menggunakan IIFE, kami telah mengikat variabel penghitung ke fungsi yang dipanggil dan ditutup tetapi masih dapat diakses oleh fungsi anak yang menambahkannya. Karena kami tidak dapat mengakses variabel penghitung dari luar ekspresi fungsi, kami menjadikannya pribadi melalui manipulasi pelingkupan.
Menggunakan penutupan, kita dapat membuat objek dengan bagian pribadi dan publik. Ini disebut modul dan sangat berguna kapan pun kita ingin menyembunyikan bagian tertentu dari suatu objek dan hanya mengekspos antarmuka ke pengguna modul. Mari kita tunjukkan ini dalam sebuah contoh:
// through the use of a closure we expose an object // as a public API which manages the private objects array var collection = (function() { // private members var objects = []; // public members return { addObject: function(object) { objects.push(object); }, removeObject: function(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } }, getObjects: function() { return JSON.parse(JSON.stringify(objects)); } }; })(); collection.addObject("Bob"); collection.addObject("Alice"); collection.addObject("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(collection.getObjects()); collection.removeObject("Alice"); // prints ["Bob", "Franck"] console.log(collection.getObjects());
Hal paling berguna yang diperkenalkan oleh pola ini adalah pemisahan yang jelas antara bagian pribadi dan publik dari suatu objek, yang merupakan konsep yang sangat mirip dengan pengembang yang berasal dari latar belakang berorientasi objek klasik.
Namun, tidak semuanya begitu sempurna. Bila Anda ingin mengubah visibilitas anggota, Anda perlu mengubah kode di mana pun Anda telah menggunakan anggota ini karena sifat yang berbeda dalam mengakses bagian publik dan pribadi. Juga, metode yang ditambahkan ke objek setelah pembuatannya tidak dapat mengakses anggota pribadi objek.
Mengungkap Pola Modul
Pola ini merupakan perbaikan yang dilakukan terhadap pola modul seperti yang digambarkan di atas. Perbedaan utama adalah bahwa kita menulis seluruh logika objek dalam ruang lingkup pribadi modul dan kemudian hanya mengekspos bagian yang kita inginkan untuk publik dengan mengembalikan objek anonim. Kami juga dapat mengubah penamaan anggota pribadi saat memetakan anggota pribadi ke anggota publik yang sesuai.
// we write the entire object logic as private members and // expose an anonymous object which maps members we wish to reveal // to their corresponding public members var namesCollection = (function() { // private members var objects = []; function addObject(object) { objects.push(object); } function removeObject(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } } function getObjects() { return JSON.parse(JSON.stringify(objects)); } // public members return { addName: addObject, removeName: removeObject, getNames: getObjects }; })(); namesCollection.addName("Bob"); namesCollection.addName("Alice"); namesCollection.addName("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(namesCollection.getNames()); namesCollection.removeName("Alice"); // prints ["Bob", "Franck"] console.log(namesCollection.getNames());
Pola modul mengungkapkan adalah salah satu dari setidaknya tiga cara di mana kita dapat mengimplementasikan pola modul. Perbedaan antara pola modul pengungkapan dan varian lain dari pola modul terutama dalam cara anggota publik dirujuk. Hasilnya, pola modul yang terbuka jauh lebih mudah digunakan dan dimodifikasi; namun, mungkin terbukti rapuh dalam skenario tertentu, seperti menggunakan objek RMP sebagai prototipe dalam rantai pewarisan. Situasi bermasalah adalah sebagai berikut:
- Jika kita memiliki fungsi privat yang mengacu pada fungsi publik, kita tidak dapat mengesampingkan fungsi publik, karena fungsi privat akan terus merujuk ke implementasi fungsi privat, sehingga memperkenalkan bug ke dalam sistem kita.
- Jika kita memiliki anggota publik yang menunjuk ke variabel pribadi, dan mencoba untuk menimpa anggota publik dari luar modul, fungsi lain masih akan merujuk ke nilai pribadi variabel, memperkenalkan bug ke dalam sistem kita.
Pola Tunggal
Pola singleton digunakan dalam skenario ketika kita membutuhkan tepat satu instance dari suatu kelas. Misalnya, kita perlu memiliki objek yang berisi beberapa konfigurasi untuk sesuatu. Dalam kasus ini, tidak perlu membuat objek baru setiap kali objek konfigurasi diperlukan di suatu tempat di sistem.
var singleton = (function() { // private singleton value which gets initialized only once var config; function initializeConfiguration(values){ this.randomNumber = Math.random(); values = values || {}; this.number = values.number || 5; this.size = values.size || 10; } // we export the centralized method for retrieving the singleton value return { getConfig: function(values) { // we initialize the singleton value only once if (config === undefined) { config = new initializeConfiguration(values); } // and return the same config value wherever it is asked for return config; } }; })(); var configObject = singleton.getConfig({ "size": 8 }); // prints number: 5, size: 8, randomNumber: someRandomDecimalValue console.log(configObject); var configObject1 = singleton.getConfig({ "number": 8 }); // prints number: 5, size: 8, randomNumber: same randomDecimalValue as in first config console.log(configObject1);
Seperti yang Anda lihat dalam contoh, nomor acak yang dihasilkan selalu sama, serta nilai konfigurasi yang dikirim.
Penting untuk dicatat bahwa titik akses untuk mengambil nilai tunggal hanya perlu satu dan sangat terkenal. Kelemahan menggunakan pola ini adalah agak sulit untuk diuji.
Pola Pengamat
Pola pengamat adalah alat yang sangat berguna ketika kita memiliki skenario di mana kita perlu meningkatkan komunikasi antara bagian-bagian yang berbeda dari sistem kita secara optimal. Ini mempromosikan kopling longgar antara objek.
Ada berbagai versi dari pola ini, tetapi dalam bentuknya yang paling dasar, kita memiliki dua bagian utama dari pola tersebut. Yang pertama adalah subjek dan yang kedua adalah pengamat.
Subjek menangani semua operasi mengenai topik tertentu yang menjadi langganan pengamat. Operasi ini membuat pengamat berlangganan topik tertentu, menghentikan langganan pengamat dari topik tertentu, dan memberi tahu pengamat tentang topik tertentu saat sebuah acara dipublikasikan.
Namun, ada variasi dari pola ini yang disebut pola penerbit/pelanggan, yang akan saya gunakan sebagai contoh di bagian ini. Perbedaan utama antara pola pengamat klasik dan pola penerbit/pelanggan adalah bahwa penerbit/pelanggan mempromosikan kopling yang lebih longgar daripada pola pengamat.
Dalam pola pengamat, subjek memegang referensi ke pengamat yang berlangganan dan memanggil metode langsung dari objek itu sendiri sedangkan, dalam pola penerbit/pelanggan, kami memiliki saluran, yang berfungsi sebagai jembatan komunikasi antara pelanggan dan penerbit. Penerbit menjalankan acara dan cukup menjalankan fungsi panggilan balik yang dikirim untuk acara itu.
Saya akan menampilkan contoh singkat dari pola penerbit/pelanggan, tetapi bagi mereka yang tertarik, contoh pola pengamat klasik dapat dengan mudah ditemukan secara online.
var publisherSubscriber = {}; // we send in a container object which will handle the subscriptions and publishings (function(container) { // the id represents a unique subscription id to a topic var id = 0; // we subscribe to a specific topic by sending in // a callback function to be executed on event firing container.subscribe = function(topic, f) { if (!(topic in container)) { container[topic] = []; } container[topic].push({ "id": ++id, "callback": f }); return id; } // each subscription has its own unique ID, which we use // to remove a subscriber from a certain topic container.unsubscribe = function(topic, id) { var subscribers = []; for (var subscriber of container[topic]) { if (subscriber.id !== id) { subscribers.push(subscriber); } } container[topic] = subscribers; } container.publish = function(topic, data) { for (var subscriber of container[topic]) { // when executing a callback, it is usually helpful to read // the documentation to know which arguments will be // passed to our callbacks by the object firing the event subscriber.callback(data); } } })(publisherSubscriber); var subscriptionID1 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Bob's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID2 = publisherSubscriber.subscribe("mouseHovered", function(data) { console.log("I am Bob's callback function for a hovered mouse event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID3 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Alice's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); // NOTE: after publishing an event with its data, all of the // subscribed callbacks will execute and will receive // a data object from the object firing the event // there are 3 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"}); // we unsubscribe from an event by removing the subscription ID publisherSubscriber.unsubscribe("mouseClicked", subscriptionID3); // there are 2 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"});
Pola desain ini berguna dalam situasi ketika kita perlu melakukan beberapa operasi pada satu peristiwa yang dipicu. Bayangkan Anda memiliki skenario di mana kami perlu membuat beberapa panggilan AJAX ke layanan back-end dan kemudian melakukan panggilan AJAX lainnya tergantung pada hasilnya. Anda harus menyarangkan panggilan AJAX satu sama lain, mungkin memasuki situasi yang dikenal sebagai neraka panggilan balik. Menggunakan pola penerbit/pelanggan adalah solusi yang jauh lebih elegan.
Kelemahan menggunakan pola ini adalah pengujian yang sulit dari berbagai bagian sistem kami. Tidak ada cara yang elegan bagi kami untuk mengetahui apakah bagian sistem yang berlangganan berperilaku seperti yang diharapkan atau tidak.
Pola Perantara
Kami akan membahas secara singkat sebuah pola yang juga sangat berguna ketika berbicara tentang sistem yang dipisahkan. Ketika kita memiliki skenario di mana beberapa bagian dari suatu sistem perlu berkomunikasi dan dikoordinasikan, mungkin solusi yang baik adalah dengan memperkenalkan mediator.
Mediator adalah objek yang digunakan sebagai titik sentral untuk komunikasi antara bagian-bagian yang berbeda dari suatu sistem dan menangani alur kerja di antara mereka. Sekarang, penting untuk ditekankan bahwa itu menangani alur kerja. Mengapa ini penting?
Karena ada kemiripan yang besar dengan pola publisher/subscriber. Anda mungkin bertanya pada diri sendiri, OK, jadi kedua pola ini membantu menerapkan komunikasi yang lebih baik antar objek… Apa bedanya?
Perbedaannya adalah bahwa mediator menangani alur kerja, sedangkan penerbit/pelanggan menggunakan sesuatu yang disebut jenis komunikasi "api dan lupakan". Penerbit/pelanggan hanyalah agregator peristiwa, artinya ia hanya menangani pengaktifan peristiwa dan memberi tahu pelanggan yang benar peristiwa mana yang dipicu. Agregator acara tidak peduli apa yang terjadi setelah suatu peristiwa dipecat, yang tidak terjadi pada mediator.
Contoh mediator yang bagus adalah tipe antarmuka wizard. Katakanlah Anda memiliki proses pendaftaran yang besar untuk sistem yang telah Anda kerjakan. Seringkali, ketika banyak informasi diperlukan dari pengguna, merupakan praktik yang baik untuk memecahnya menjadi beberapa langkah.
Dengan cara ini, kode akan jauh lebih bersih (lebih mudah dirawat) dan pengguna tidak kewalahan dengan banyaknya informasi yang diminta hanya untuk menyelesaikan pendaftaran. Mediator adalah objek yang akan menangani langkah-langkah pendaftaran, dengan mempertimbangkan kemungkinan alur kerja yang berbeda yang mungkin terjadi karena fakta bahwa setiap pengguna berpotensi memiliki proses pendaftaran yang unik.
Manfaat nyata dari pola desain ini adalah peningkatan komunikasi antara berbagai bagian sistem, yang sekarang semuanya berkomunikasi melalui mediator dan basis kode yang lebih bersih.
Kelemahannya adalah sekarang kami telah memperkenalkan satu titik kegagalan ke dalam sistem kami, yang berarti jika mediator kami gagal, seluruh sistem dapat berhenti bekerja.
Pola Prototipe
Seperti yang telah kami sebutkan di seluruh artikel, JavaScript tidak mendukung kelas dalam bentuk aslinya. Pewarisan antar objek diimplementasikan menggunakan pemrograman berbasis prototipe.
Ini memungkinkan kita untuk membuat objek yang dapat berfungsi sebagai prototipe untuk objek lain yang sedang dibuat. Objek prototipe digunakan sebagai cetak biru untuk setiap objek yang dibuat oleh konstruktor.
Seperti yang telah kita bicarakan di bagian sebelumnya, mari kita tunjukkan contoh sederhana tentang bagaimana pola ini dapat digunakan.
var personPrototype = { sayHi: function() { console.log("Hello, my name is " + this.name + ", and I am " + this.age); }, sayBye: function() { console.log("Bye Bye!"); } }; function Person(name, age) { name = name || "John Doe"; age = age || 26; function constructorFunction(name, age) { this.name = name; this.age = age; }; constructorFunction.prototype = personPrototype; var instance = new constructorFunction(name, age); return instance; } var person1 = Person(); var person2 = Person("Bob", 38); // prints out Hello, my name is John Doe, and I am 26 person1.sayHi(); // prints out Hello, my name is Bob, and I am 38 person2.sayHi();
Take notice how prototype inheritance makes a performance boost as well because both objects contain a reference to the functions which are implemented in the prototype itself, instead of in each of the objects.
Command Pattern
The command pattern is useful in cases when we want to decouple objects executing the commands from objects issuing the commands. For example, imagine a scenario where our application is using a large number of API service calls. Then, let's say that the API services change. We would have to modify the code wherever the APIs that changed are called.
This would be a great place to implement an abstraction layer, which would separate the objects calling an API service from the objects which are telling them when to call the API service. This way, we avoid modification in all of the places where we have a need to call the service, but rather have to change only the objects which are making the call itself, which is only one place.
As with any other pattern, we have to know when exactly is there a real need for such a pattern. We need to be aware of the tradeoff we are making, as we are adding an additional abstraction layer over the API calls, which will reduce performance but potentially save a lot of time when we need to modify objects executing the commands.
// the object which knows how to execute the command var invoker = { add: function(x, y) { return x + y; }, subtract: function(x, y) { return x - y; } } // the object which is used as an abstraction layer when // executing commands; it represents an interface // toward the invoker object var manager = { execute: function(name, args) { if (name in invoker) { return invoker[name].apply(invoker, [].slice.call(arguments, 1)); } return false; } } // prints 8 console.log(manager.execute("add", 3, 5)); // prints 2 console.log(manager.execute("subtract", 5, 3));
Facade Pattern
The facade pattern is used when we want to create an abstraction layer between what is shown publicly and what is implemented behind the curtain. It is used when an easier or simpler interface to an underlying object is desired.
A great example of this pattern would be selectors from DOM manipulation libraries such as jQuery, Dojo, or D3. You might have noticed using these libraries that they have very powerful selector features; you can write in complex queries such as:
jQuery(".parent .child div.span")
It simplifies the selection features a lot, and even though it seems simple on the surface, there is an entire complex logic implemented under the hood in order for this to work.
We also need to be aware of the performance-simplicity tradeoff. It is desirable to avoid extra complexity if it isn't beneficial enough. In the case of the aforementioned libraries, the tradeoff was worth it, as they are all very successful libraries.
Langkah selanjutnya
Design patterns are a very useful tool which any senior JavaScript developer should be aware of. Knowing the specifics regarding design patterns could prove incredibly useful and save you a lot of time in any project's lifecycle, especially the maintenance part. Modifying and maintaining systems written with the help of design patterns which are a good fit for the system's needs could prove invaluable.
In order to keep the article relatively brief, we will not be displaying any more examples. For those interested, a great inspiration for this article came from the Gang of Four book Design Patterns: Elements of Reusable Object-Oriented Software and Addy Osmani's Learning JavaScript Design Patterns . I highly recommend both books.