
Inversion of Control adalah prinsip pemrograman yang cukup mudah dipahami, yang, pada saat yang sama, dapat secara signifikan meningkatkan kode Anda. Artikel ini akan menunjukkan cara menerapkan Kontrol Pembalikan dalam JavaScript dan Reactjs.
Jika Anda sudah menulis kode yang digunakan di lebih dari satu tempat, maka Anda terbiasa dengan situasi ini:
- Anda membuat potongan kode yang dapat digunakan kembali (bisa berupa fungsi, React komponen, React hook, dll.) Dan membagikannya (untuk kolaborasi atau penerbitan dalam sumber terbuka).
- Seseorang meminta Anda untuk menambahkan fungsionalitas baru. Kode Anda tidak mendukung fungsionalitas yang diusulkan, tetapi bisa jika Anda membuat perubahan kecil.
- Anda menambahkan argumen / prop / opsi baru ke kode Anda dan logikanya yang terkait agar fitur baru ini berfungsi.
- Ulangi langkah 2 dan 3 beberapa kali (atau banyak, berkali-kali).
- Sekarang kode reusable Anda sulit digunakan dan dipelihara.
Apa sebenarnya yang membuat kode mimpi buruk untuk digunakan dan dipelihara? Ada beberapa aspek yang dapat membuat kode Anda bermasalah:
- Ukuran dan / atau kinerja paket: Hanya lebih banyak kode untuk dijalankan pada perangkat dapat menyebabkan kinerja yang buruk. Terkadang ini dapat menyebabkan orang menolak untuk menggunakan kode Anda.
- Sulit dipertahankan: Sebelumnya, kode Anda yang dapat digunakan kembali hanya memiliki beberapa opsi, dan berfokus pada melakukan satu hal dengan baik, tetapi sekarang dapat melakukan banyak hal yang berbeda, dan Anda perlu mendokumentasikan semuanya. Selain itu, orang-orang akan mulai bertanya kepada Anda tentang cara menggunakan kode Anda untuk kasus penggunaan tertentu yang mungkin atau tidak dapat dibandingkan dengan kasus penggunaan yang telah Anda tambahkan dukungannya. Anda bahkan mungkin memiliki dua kasus penggunaan yang hampir identik yang sedikit berbeda, jadi Anda harus menjawab pertanyaan tentang apa yang terbaik untuk digunakan dalam situasi tertentu.
- Kompleksitas implementasi : Setiap kali ini bukan hanya
if
, masing-masing cabang logika kode Anda hidup berdampingan dengan cabang-cabang logika yang ada. Bahkan, situasi mungkin terjadi ketika Anda mencoba mempertahankan kombinasi argumen / opsi / alat peraga yang bahkan tidak digunakan oleh siapa pun, tetapi Anda masih perlu mempertimbangkan opsi yang memungkinkan, karena Anda tidak tahu pasti apakah ada orang yang akan menggunakan kombinasi ini atau tidak. - API canggih : Setiap argumen / opsi / prop baru yang Anda tambahkan ke kode yang dapat digunakan kembali menyulitkan Anda untuk menggunakannya, karena sekarang Anda memiliki README besar atau situs tempat semua fungsionalitas yang tersedia didokumentasikan, dan orang-orang harus mempelajari semua ini untuk penggunaan yang efektif kode Anda. Menggunakannya tidak nyaman, karena kompleksitas API Anda menembus kode pengembang yang menggunakannya, yang mempersulit kodenya.
Akibatnya, semua orang menderita. Perlu dicatat bahwa implementasi program akhir adalah bagian penting dari pengembangan. Tapi alangkah baiknya jika kita lebih memikirkan implementasi abstraksi kita (baca tentang "pemrograman AHA" ). Apakah ada cara agar kita dapat mengurangi masalah dengan kode yang dapat digunakan kembali dan masih menuai manfaat menggunakan abstraksi?
Kontrol inversi
Pembalikan kontrol adalah prinsip yang sangat menyederhanakan pembuatan dan penggunaan abstraksi. Inilah yang dikatakan Wikipedia tentang hal itu:
... dalam pemrograman tradisional, kode pengguna yang menyatakan tujuan program disebut dalam pustaka yang dapat digunakan kembali untuk memecahkan masalah umum, tetapi dengan kontrol inversi, ini adalah lingkungan yang memanggil kode khusus atau tugas khusus,
Pikirkan seperti ini: "Kurangi fungsionalitas abstraksi Anda, dan buat agar pengguna Anda dapat mengimplementasikan fungsionalitas yang mereka butuhkan." Ini mungkin tampak seperti absurditas, karena kami menggunakan abstraksi untuk menyembunyikan tugas yang kompleks dan berulang, dan dengan demikian membuat kode kami lebih "bersih" dan "rapi". Tetapi, seperti yang telah kita lihat di atas, abstraksi tradisional tidak selalu menyederhanakan kode.
Apa itu Pembalikan Manajemen dalam kode?
Untuk memulai, berikut adalah contoh yang sangat dibuat-buat:
Sekarang mari kita mainkan "siklus hidup abstraksi" yang khas dengan menambahkan kasus penggunaan baru ke abstraksi ini dan "meningkatkannya tanpa berpikir" untuk mendukung kasus penggunaan baru ini:
Jadi, program kami hanya berfungsi dengan enam use case, tetapi sebenarnya kami mendukung setiap kemungkinan kombinasi fungsi, dan ada sebanyak 25 kombinasi tersebut (jika saya hitung dengan benar).
Secara umum, ini adalah abstraksi yang cukup sederhana. Tapi itu bisa disederhanakan. Sering terjadi bahwa abstraksi di mana fungsionalitas baru ditambahkan dapat sangat disederhanakan untuk kasus penggunaan yang sebenarnya didukung. Sayangnya, segera setelah abstraksi mulai mendukung sesuatu (misalnya, mengeksekusi { filterZero: true, filterUndefined: false }
), kami takut untuk menghapus fungsionalitas ini karena fakta bahwa itu dapat merusak kode yang bergantung padanya.
Kami bahkan menulis tes untuk kasus penggunaan yang sebenarnya tidak kami miliki, hanya karena abstraksi kami mendukung skenario ini, dan kami mungkin perlu melakukan ini di masa depan. Dan ketika kasus penggunaan ini atau lainnya menjadi tidak perlu bagi kami, kami tidak menghapus dukungan mereka, karena kami hanya melupakannya, atau berpikir bahwa itu mungkin berguna bagi kami di masa depan, atau hanya kami takut untuk menghancurkan sesuatu.
Oke, sekarang mari kita menulis abstraksi yang lebih rumit untuk fungsi ini dan menerapkan metode inversi kontrol untuk mendukung semua kasus penggunaan yang kita butuhkan:
Hebat! Ternyata jauh lebih mudah. Kami baru saja mengalihkan kontrol atas fungsi, melewati tanggung jawab untuk memutuskan elemen mana yang jatuh ke dalam array baru, dari fungsi filter
ke fungsi yang memanggil fungsi filter. Perhatikan bahwa fungsi filter
masih merupakan abstraksi yang berguna, tetapi sekarang ini jauh lebih fleksibel.
Tetapi apakah versi sebelumnya dari abstraksi ini begitu buruk? Mungkin tidak. Tetapi karena kami mengalihkan kontrol, kami sekarang dapat mendukung kasus penggunaan yang jauh lebih unik:
filter( [ {name: 'dog', legs: 4, mammal: true}, {name: 'dolphin', legs: 0, mammal: true}, {name: 'eagle', legs: 2, mammal: false}, {name: 'elephant', legs: 4, mammal: true}, {name: 'robin', legs: 2, mammal: false}, {name: 'cat', legs: 4, mammal: true}, {name: 'salmon', legs: 0, mammal: false}, ], animal => animal.legs === 0, )
Bayangkan saja jika Anda perlu menambahkan dukungan untuk kasus penggunaan ini tanpa menerapkan inversi kontrol? Ya, itu hanya konyol.
API buruk?
Salah satu keluhan paling umum yang saya dengar dari orang-orang mengenai API yang menggunakan inversi kontrol adalah: "Ya, tapi sekarang lebih sulit untuk digunakan daripada sebelumnya." Ambil contoh ini:
Ya, salah satu opsi jelas lebih mudah digunakan daripada yang lain. Tetapi salah satu manfaat dari inversi kontrol adalah Anda dapat menggunakan API yang menggunakan inversi kontrol untuk mengimplementasikan kembali API lama Anda. Ini biasanya cukup sederhana. Sebagai contoh:
function filterWithOptions( array, { filterNull = true, filterUndefined = true, filterZero = false, filterEmptyString = false, } = {}, ) { return filter( array, element => !( (filterNull && element === null) || (filterUndefined && element === undefined) || (filterZero && element === 0) || (filterEmptyString && element === '') ), ) }
Keren ya Dengan cara ini, kita dapat membuat abstraksi di atas API tempat kontrol inversi diterapkan, dan karenanya membuat API yang lebih sederhana. Dan jika tidak ada cukup kasus penggunaan di API "sederhana" kami, maka pengguna kami dapat menggunakan blok bangunan yang sama yang kami gunakan untuk membuat API tingkat tinggi untuk mengembangkan solusi untuk tugas yang lebih kompleks. Mereka tidak perlu meminta kami untuk menambahkan fungsi baru ke filterWithOptions
dan menunggu untuk diimplementasikan. Mereka sudah memiliki alat yang dengannya mereka dapat secara mandiri mengembangkan fungsionalitas tambahan yang mereka butuhkan.
Dan, hanya untuk penggemar:
function filterByLegCount(array, legCount) { return filter(array, animal => animal.legs === legCount) } filterByLegCount( [ {name: 'dog', legs: 4, mammal: true}, {name: 'dolphin', legs: 0, mammal: true}, {name: 'eagle', legs: 2, mammal: false}, {name: 'elephant', legs: 4, mammal: true}, {name: 'robin', legs: 2, mammal: false}, {name: 'cat', legs: 4, mammal: true}, {name: 'salmon', legs: 0, mammal: false}, ], 0, )
Anda dapat membuat fungsionalitas khusus untuk situasi apa pun yang sering terjadi pada Anda.
Contoh kehidupan nyata
Jadi, ini berfungsi dalam kasus-kasus sederhana, tetapi apakah konsep ini cocok untuk kehidupan nyata? Yah, kemungkinan besar Anda terus menggunakan inversi kontrol. Misalnya, fungsi Array.prototype.filter
menerapkan kontrol terbalik. Seperti fungsi Array.prototype.map
.
Ada berbagai pola yang mungkin sudah Anda kenal, dan yang merupakan salah satu bentuk inversi kontrol.
Berikut adalah dua pola favorit saya yang menunjukkan ini adalah "Komponen Senyawa" dan "Pengurang Negara" . Di bawah ini adalah contoh singkat bagaimana pola-pola ini dapat diterapkan.
Komponen Senyawa
Misalkan Anda ingin membuat komponen Menu
yang memiliki tombol untuk membuka menu dan daftar item menu yang akan ditampilkan ketika Anda mengklik tombol. Kemudian, ketika item dipilih, ia akan melakukan beberapa tindakan. Biasanya, untuk mengimplementasikan ini, mereka hanya membuat alat peraga:
function App() { return ( <Menu buttonContents={ <> Actions <span aria-hidden>▾</span> </> } items={[ {contents: 'Download', onSelect: () => alert('Download')}, {contents: 'Create a Copy', onSelect: () => alert('Create a Copy')}, {contents: 'Delete', onSelect: () => alert('Delete')}, ]} /> ) }
Ini memungkinkan kita untuk mengkonfigurasi banyak hal dalam item menu. Tetapi bagaimana jika kita ingin menyisipkan baris sebelum item menu Hapus? Haruskah kita menambahkan opsi khusus ke objek yang terkait dengan items
? Yah, saya tidak tahu, misalnya: precedeWithLine
? Gagasan begitu-begitu.
Mungkin membuat jenis item menu khusus, misalnya {contents: <hr />}
. Saya pikir itu akan berhasil, tetapi kemudian kita harus menangani kasus di mana tidak ada pada onSelect
. Dan sejujurnya, ini adalah API yang sangat canggung.
Ketika Anda berpikir tentang cara membuat API yang bagus untuk orang yang mencoba melakukan sesuatu yang sedikit berbeda, alih-alih meraih pernyataan if
, cobalah untuk membalikkan kontrol. Bagaimana jika kita mentransfer tanggung jawab untuk visualisasi menu kepada pengguna? Kami menggunakan salah satu kekuatan dari reaksi:
function App() { return ( <Menu> <MenuButton> Actions <span aria-hidden>▾</span> </MenuButton> <MenuList> <MenuItem onSelect={() => alert('Download')}>Download</MenuItem> <MenuItem onSelect={() => alert('Copy')}>Create a Copy</MenuItem> <MenuItem onSelect={() => alert('Delete')}>Delete</MenuItem> </MenuList> </Menu> ) }
Poin penting untuk diperhatikan adalah bahwa tidak ada keadaan komponen yang terlihat oleh pengguna. Negara secara implisit dibagi antara komponen-komponen ini. Ini adalah nilai inti dari pola komponen. Menggunakan kesempatan ini, kami memberikan kontrol atas rendering kepada pengguna komponen kami, dan sekarang menambahkan garis ekstra (atau yang lain) adalah tindakan yang sederhana dan intuitif. Tidak ada dokumentasi tambahan, tidak ada fungsi tambahan, tidak ada kode atau tes tambahan. Semua orang menang.
Anda dapat membaca lebih lanjut tentang pola ini di sini . Terima kasih kepada Ryan Florence , yang mengajari saya ini.
Peredam Negara
Saya datang dengan pola ini untuk menyelesaikan masalah pengaturan komponen logika. Anda dapat membaca lebih banyak tentang situasi itu di blog saya , The State Reducer Pattern , tetapi intinya adalah bahwa saya memiliki perpustakaan pencarian / autocomplete / type-in yang disebut Downshift
, dan salah satu pengguna perpustakaan sedang mengembangkan versi komponen multi-pilihan , karena itu dia ingin menu tetap terbuka bahkan setelah memilih item.
Logika di balik Downshift
menyarankan bahwa setelah pilihan dibuat, menu harus ditutup. Seorang pengguna perpustakaan yang perlu mengubah fungsinya menyarankan menambahkan prop closeOnSelection
. Saya menolak tawaran ini, karena dulu saya sudah melewati jalan menuju apropalapse , dan ingin menghindarinya.
Sebagai gantinya, saya membuat API sehingga pengguna sendiri dapat mengontrol bagaimana perubahan status terjadi. Pikirkan peredam keadaan sebagai keadaan fungsi yang dipanggil setiap kali keadaan komponen berubah, dan memberikan pengembang aplikasi kemampuan untuk mempengaruhi perubahan keadaan yang akan terjadi.
Contoh menggunakan perpustakaan Downshift
sehingga tidak menutup menu setelah pengguna mengklik item yang dipilih:
function stateReducer(state, changes) { switch (changes.type) { case Downshift.stateChangeTypes.keyDownEnter: case Downshift.stateChangeTypes.clickItem: return { ...changes,
Setelah kami menambahkan prop ini, kami mulai menerima permintaan JAUH lebih sedikit untuk menambahkan pengaturan baru untuk komponen ini. Komponen telah menjadi lebih fleksibel, dan telah menjadi lebih mudah bagi pengembang untuk mengkonfigurasinya sesuai kebutuhan.
Render Props
Layak disebutkan adalah pola "render props" . Pola ini adalah contoh ideal menggunakan inversi kontrol, tetapi kami terutama tidak membutuhkannya. Baca lebih lanjut di sini: mengapa kita tidak membutuhkan Render Props lagi .
Peringatan
Pembalikan kontrol adalah cara hebat untuk mengatasi masalah kesalahpahaman tentang bagaimana kode Anda akan digunakan di masa depan. Tetapi sebelum selesai, saya ingin memberi Anda beberapa saran.
Mari kita kembali ke contoh kita:
Bagaimana jika ini yang kita butuhkan dari fungsi filter
? Dan kami tidak pernah menghadapi situasi ketika kami perlu menyaring apa pun kecuali null
dan undefined
? Dalam hal ini, menambahkan inversi kontrol untuk kasus penggunaan tunggal hanya akan menyulitkan kode dan tidak membawa banyak manfaat.
Seperti halnya abstraksi apa pun, berhati-hatilah untuk menerapkan prinsip Pemrograman AHA dan menghindari abstraksi yang terburu-buru!
Kesimpulan
Semoga artikel ini bermanfaat bagi Anda. Saya menunjukkan bagaimana Anda dapat menerapkan konsep Pembalikan Kontrol dalam suatu reaksi. Konsep ini, tentu saja, berlaku tidak hanya untuk Bereaksi (seperti yang kita lihat dengan fungsi filter
). Lain kali Anda melihat bahwa Anda menambahkan if
lain ke fungsi coreBusinessLogic
aplikasi Anda, pikirkan bagaimana Anda dapat membalikkan kontrol dan mentransfer logika ke tempat itu digunakan (atau, jika digunakan di beberapa tempat, Anda dapat membuat abstraksi yang lebih khusus untuk ini) kasus tertentu).
Jika mau, Anda bisa bermain dengan contoh dari artikel di CodeSandbox .
Selamat mencoba dan terima kasih atas perhatiannya!
PS. Jika Anda menikmati artikel ini, Anda mungkin menikmati pembicaraan ini: youtube Kent C Dodds - Simply React