Cognitive Load dalam Software Development
Tulisan ini merupakan versi terjemahan Bahasa Indonesia dari repo https://github.com/zakirullin/cognitive-load, jadi please kasi bintang buat penulis aslinya yak 👍🏽 🔥
Begitu banyak istilah, jargon, dan best practice dalam software engineering yang bertebaran di internet sehingga terkadang kita dibuat bingung harus mengikuti yang mana, tetapi untuk saat ini mari kita fokus pada masalah yang lebih krusial. Masalah yang lebih krusial untuk dipikirkan bagi seorang developer adalah seberapa banyak perasaan “bingung” yang dirasakan oleh developer saat membaca kode.
Kebingungan menyebabkan pemborosan waktu dan uang. Kebingungan disebabkan oleh cognitive load yang tinggi. Cognitive load dalam software engineering bukanlah konsep yang “wah” atau dibuat-buat, ini nyata adanya, dan kita bisa merasakannya.
Ketika kita menghabiskan lebih banyak waktu untuk membaca dan memahami sebuah kode daripada menuliskan-nya, kita seharusnya bertanya kepada diri kita sendiri: “apakah kita menyematkan cognitive load berlebih ke dalam kode kita ?”.
Apa itu Cognitive Load dalam Software Development?
Cognitive load dalam software development adalah seberapa banyak seorang developer perlu mengerahkan brain power mereka untuk menyelesaikan suatu tugas. Kita sebagai seorang developer, sebisa mungkin harus mengurangi cognitive load dalam proyek atau daily task yang sedang kita garap.
Saat membaca sebuah kode, kita menyimpan berbagai fakta di dalam pikiran kita, seperti logika kondisional, nilai-nilai dari sebuah variabel, component interaction terkait, changes dan dependencies pada kode lain, dan sebagainya. Orang rata-rata dapat menyimpan kira-kira empat fakta yang tidak terkait dalam memori kerja mereka. Begitu cognitive load mencapai ambang batas ini, diperlukan brain power yang cukup signifikan untuk memahami suatu hal.
Katakanlah kita diminta untuk melakukan changes pada sebuah project yang tidak kita kenal atau bahkan sama sekali tidak pernah kita sentuh sebelumnya. Kita diberi tahu bahwa seorang software engineer yang sangat jenius telah berkontribusi pada proyek tersebut. Banyak implementasi software architecture yang keren, library canggih, dan menggunakan tech stack terkini. Dengan kata lain, developer sebelumnya telah menciptakan cognitive load yang tinggi bagi kita.
Bagian yang tricky dari kasus ini adalah developer sebelumnya mungkin tidak mengalami cognitive load yang tinggi karena sudah terbiasa dan mengerti dengan proyek yang dikembangkannya itu (yaiyalah dia yang buat masa dia yang ga ngerti apa yang dia buat hehe).
Here’s the trick: ketika mengenalkan seorang newcomer pada proyek kalian, coba ukur seberapa besar rasa bingung yang mereka alami (pair programming mungkin dapat membantu). Jika mereka bingung selama lebih dari ~40 menit berturut-turut, maka kalian memiliki hal-hal yang perlu diperbaiki di dalam proyek tersebut.
Jenis-jenis Cognitive Load pada Software Development
Secara umum, cognitive load pada software development ada 2 jenis, yaitu:
- Intrinsic: merupakan suatu jenis cognitive load yang disebabkan oleh kesulitan/kompleksitas yang melekat dari suatu task. Hal ini tidak dapat dikurangi, karena ini merupakan inti dari pengembangan software.
- Extraneous: merupakan suatu jenis cognitive load yang diciptakan oleh bagaimana cara suatu informasi disajikan. Disebabkan oleh faktor-faktor yang tidak relevan secara langsung dengan task, seperti penambahan unnecessary complexity pada suatu task atau over-engineering suatu task yang seharusnya menghasilkan solusi straightforward. Dapat dikurangi secara signifikan.
Pada tulisan ini akan fokus membahas jenis cognitive load Extraneous. Mari langsung beralih ke contoh-contoh dari cognitive load Extraneous.
Kita akan merujuk pada tingkat beban kognitif sebagai berikut:
🧠 : working memory lagi fresh, cognitive load 0
🧠++ : 2 fakta dalam working memory kita, cognitive load meningkat
🤯 : kelebihan working memory (overload), lebih dari 4 fakta
1. Inheritance Nightmare
Kalian diminta untuk mengubah beberapa hal untuk admin users: 🧠
AdminController extend UserController extend GuestController extend BaseController
Ohh, bagian fungsionalitas ada di BaseController
, mari kita lihat: 🧠+
Logika dasar dari role diperkenalkan di GuestController
: 🧠++
Beberapa hal dimodifikasi di UserController
: 🧠+++
Akhirnya kita di sini, AdminController
, mari kita ngoding! 🧠++++
Oh, tunggu! Ada juga SuperUserController
yang meng-extend AdminController
. Dengan memodifikasi AdminController
, kita bisa merusak beberapa hal di kelas yang diwariskan, jadi mari kita lihat SuperUserController
terlebih dahulu: 🤯
Preferensikan composition dibanding inheritance. Pada bahasan ini, tidak akan masuk ke detail — sudah banyak materi di luar sana.
2. Complex Conditionals
if val > someConstant // 🧠+
&& (condition2 || condition3) // 🧠+++
// kondisi sebelumnya bernilai true,
// salah satu dari c2 atau c3 pasti bernilai true
&& (condition4 && !condition5) { // 🤯
// sudah mulai kacau sampai sini
...
}
Buatlah variabel perantara dengan nama yang memiliki makna:
isValid = var > someConstant
isAllowed = condition2 || condition3
isSecure = condition4 && !condition5 // 🧠
// kita tidak perlu mengingat kondisinya
// karena nama variabel sudah deskriptif
if isValid && isAllowed && isSecure {
...
}
3. Nested ifs
if isValid { // 🧠+,
// oke, nested code berlaku untuk input yang valid
if isSecure { // 🧠++,
// kita menjalankan stuff1 untuk input yang valid dan aman
stuff1 // 🧠+++
}
stuff2 // 🧠++++,
// kita menjalankan stuff2 untuk semua input yang valid,
// kita harus ingat juga pada stuff1,
// karena itu mungkin mengganggu stuff2
}
Bandingkan pengecekan kondisi diatas dengan implementasi early return.
if !isValid // 🧠
return
// kita tidak terlalu peduli dengan earlier returns,
// jika mencapai tahap ini artinya sudah tidak ada masalah
stuff2 // 🧠+
if !isSecure // 🧠+
return
stuff1 // 🧠++
Kita bisa fokus dengan alur yang “menyenangkan” saja, yang dimana ini membebaskan working memory kita dari berbagai jenis pre-kondisi.
4. Too Many Small Methods, Classes or Modules
Kalimat seperti “method seharusnya lebih pendek dari 15 baris kode” atau “class seharusnya kecil” ternyata agak keliru.
Deep Module — interface sederhana, fungsionalitas kompleks
Shallow Module — interface relatif kompleks untuk fungsionalitas kecil yang disediakan
Mempunyai terlalu banyak modul yang dangkal bisa membuat kita sulit memahami proyek. Kita tidak hanya harus ingat tanggung jawab masing-masing modul, tetapi juga semua interaksi tiap modul. Untuk memahami tujuan modul yang dangkal, kita pertama-tama harus melihat fungsionalitas semua modul terkait 🤯.
Penulis asli artikel ini memberikan sebuah contoh, beliau memiliki dua proyek pribadi, keduanya memiliki sekitar 5.000 baris kode. Project pertama memiliki 80 kelas yang dangkal, sementara project kedua hanya memiliki 7 kelas yang dalam. Beliau tidak merawat kedua proyek ini selama satu setengah tahun.
Ketika beliau kembali, beliau menyadari bahwa sangat sulit untuk membongkar semua interaksi antara 80 kelas dalam proyek pertama. Beliau harus membangun kembali beban kognitif yang besar sebelum beliau bisa mulai coding. Di sisi lain, beliau dapat memahami proyek kedua dengan cepat, karena hanya memiliki beberapa kelas dalam dengan antarmuka yang sederhana.
“The best components are those that provide powerful functionality yet have simple interface.”
- John K. Ousterhout
Sebagai contoh terhadap quotes diatas, interface dari UNIX I/O sangat simple. Interface UNIX I/O hanya memiliki 5 basic calls:
open(path, flags, permissions)
read(fd, buffer, count)
write(fd, buffer, count)
lseek(fd, offset, referencePosition)
close(fd)
Implementasi modern dari antarmuka diatas memiliki ratusan ribu baris kode. Banyak kompleksitas disembunyikan di dalamnya. Namun, penggunaannya mudah berkat interface yang sederhana.
Contoh deep module ini diambil dari buku “A Philosophy of Software Design” karya John K. Ousterhout. Buku ini tidak hanya mencakup esensi dari kompleksitas dalam pengembangan perangkat lunak, tetapi juga memiliki interpretasi terbaik dari makalah berpengaruh Parnas, “On the Criteria To Be Used in Decomposing Systems into Modules” . Keduanya adalah bacaan penting.
Bacaan terkait lainnya: “It’s probably time to stop recommending Clean Code”, “Small Functions considered Harmful” , “Linear code is more readable”.
Jika kalian berpikir tulisan ini mendukung God Object dengan tanggung jawab yang terlalu banyak, kalian menangkap maksud tulisan ini secara keliru.
5. Too many shallow microservices
Scale-agnostic principle di atas dapat diterapkan pada arsitektur microservice juga. Terlalu banyak microservice dangkal tidak akan memberikan manfaat yang baik — saat ini, industri tech sedang menuju hal yang disebut “macroservice” atau bisa dibilang service yang tidak terlalu dangkal. Salah satu fenomena terburuk dan sulit diperbaiki adalah yang disebut sebagai distributed monolith, yang seringkali merupakan hasil dari pemisahan dangkal yang terlalu terperinci.
Penulis asli dari artikel ini pernah memberikan konsultasi kepada sebuah startup di mana tim tiga pengembang memperkenalkan 17(!) microservice. Mereka terlambat 10 bulan dari jadwal dan tidak tampak mendekati rilis publik. Setiap kebutuhan baru mengarah pada perubahan di 4+ microservice. Kesulitan diagnostik dalam ruang integrasi melonjak. Baik waktu ke pasar maupun beban kognitif sangat tinggi dan tidak dapat diterima. 🤯
Apakah ini cara yang tepat untuk menghadapi ketidakpastian sistem baru? Sangat sulit untuk menentukan batas logis yang benar di awal, dan dengan memperkenalkan terlalu banyak microservice, kita membuat situasinya semakin rumit. Satu-satunya pembenaran yang diberikan oleh mereka adalah: “Perusahaan-perusahaan FAANG membuktikan bahwa arsitektur microservice efektif.”
Sebuah monolith yang dirancang dengan baik dengan modul yang benar-benar terisolasi seringkali lebih nyaman dan fleksibel daripada sekelompok microservice. Hanya ketika kebutuhan untuk implementasi terpisah menjadi sangat penting (misalnya, skalabilitas tim pengembangan), saat itu lah sebaiknya mempertimbangkan penambahan network layer antara modul (microservice di masa depan).
6. Feature-rich Programming Languages
Kita merasa excited ketika fitur-fitur baru dirilis dalam bahasa pemrograman favorit kita. Kita menghabiskan waktu untuk mempelajari fitur-fitur ini dan membangun kode berdasarkan fitur tersebut.
Jika ada banyak fitur, kadang-kadang sampai menghabiskan waktu setengah jam bermain-main dengan beberapa baris kode untuk menggunakan satu atau beberapa fitur. Hal ini adalah pemborosan waktu. Namun, yang lebih buruk, ketika kita kembali harus fokus ke task, kita harus membuat kembali proses berpikir tersebut! 🤯
Kita tidak hanya harus memahami program yang rumit ini, tetapi kita juga harus memahami mengapa seorang programmer memutuskan bahwa ini adalah cara yang tepat untuk mendekati masalah dari fitur-fitur yang tersedia.
“Reduce cognitive load by limiting the number of choices”.
- Rob Pike
Fitur bahasa pemrograman itu bagus, selama itu bersifat ortogonal satu sama lain.
7. Business Logic and HTTP Status Codes
Di backend, kita mengembalikan:
- 401 untuk token JWT yang telah kedaluwarsa.
- 403 untuk akses yang tidak mencukupi.
- 418 untuk pengguna yang diblokir.
Tim di frontend menggunakan API backend untuk mengimplementasikan fungsionalitas login. Mereka harus sementara menciptakan beban kognitif berikut di otak mereka:
- 401 adalah untuk token JWT yang telah kedaluwarsa // 🧠+, oke, hanya diingat untuk sementara
- 403 adalah untuk akses yang tidak mencukupi // 🧠++
- 418 adalah untuk pengguna yang diblokir // 🧠+++
Frontend dev kemungkinan akan memperkenalkan variabel/fungsi seperti isTokenExpired(status)
, agar pengembang berikutnya tidak perlu membuat ulang pemetaan status -> arti
semacam ini di otak mereka.
Selanjutnya, tim QA masuk ke permainan: “Hei, saya mendapatkan status 403, apakah itu token yang kedaluwarsa atau tidak cukup akses?” Orang QA tidak dapat langsung beralih ke pengujian, karena pertama-tama mereka harus membuat kembali beban kognitif yang tim backend telah ciptakan.
Mengapa menyimpan custom mapping ini dalam memori kerja kita? Lebih baik mengabstraksi rincian bisnis dari protokol transfer HTTP, dan mengembalikan kode yang bersifat self-descriptive secara langsung dalam tubuh respons:
{
"code": "jwt_has_expired"
}
Beban kognitif di sisi frontend: 🧠 (fresh, tidak ada fakta yang disimpan dalam pikiran)
Beban kognitif di sisi QA: 🧠
Aturan yang sama berlaku untuk segala jenis status numerik (di database atau di mana pun) — lebih baik menggunakan string yang bersifat self-descriptive. Kita tidak berada di era komputer 640K untuk mengoptimalkan penyimpanan.
Orang-orang menghabiskan waktu untuk berdebat antara 401 dan 403, membuat pilihan berdasarkan tingkat pemahaman mereka. Tetapi pada akhirnya, itu tidak masuk akal. Kita bisa memisahkan kesalahan menjadi terkait pengguna atau terkait server, tetapi selain itu, hal-hal agak kabur. Sehubungan dengan mengikuti “RESTful API” yang mistis dan menggunakan berbagai kata kerja dan status HTTP, standar tersebut sebenarnya tidak ada. Satu-satunya dokumen valid tentang masalah ini adalah makalah yang diterbitkan oleh Roy Fielding, yang berasal dari tahun 2000, dan itu tidak membahas apapun tentang kata kerja dan status. Orang-orang dapat berinteraksi dengan hanya beberapa status HTTP dasar dan POST saja, dan semuanya berjalan dengan baik.
8. Abusing DRY Principle
Don’t repeat yourself — ini adalah salah satu prinsip pertama yang diajarkan kepada kita sebagai seorang software engineer. Hal ini sangat tertanam dalam diri kita sehingga kita tidak tahan dengan kenyataan dari beberapa baris kode tambahan. Meskipun pada umumnya itu adalah aturan yang baik dan mendasar, ketika digunakan berlebihan, itu mengarah pada beban kognitif yang tidak dapat kita tangani.
Saat ini, setiap orang membangun perangkat lunak berdasarkan komponen yang secara logis terpisah. Seringkali, komponen-komponen ini terdistribusi di beberapa basis kode yang mewakili layanan terpisah. Ketika kita berusaha menghilangkan pengulangan apa pun, kita mungkin berakhir dengan membuat ketergantungan yang erat antara komponen-komponen yang tidak terkait. Sebagai hasilnya, perubahan di satu bagian dapat memiliki konsekuensi tak terduga di area lain yang sepertinya tidak terkait. Ini juga dapat menghambat kemampuan untuk menggantikan atau memodifikasi komponen individual tanpa memengaruhi seluruh sistem. 🤯
Sebenarnya, masalah yang sama muncul bahkan dalam satu modul. Kita mungkin mengekstraksi fungsionalitas umum terlalu dini, berdasarkan kesamaan yang mungkin tidak benar-benar ada dalam jangka panjang. Ini dapat menghasilkan abstraksi yang tidak perlu dan sulit dimodifikasi atau diperluas.
“A little copying is better than a little dependency.”
- Rob Pike
Kita cenderung untuk tidak reinvent the wheel dengan sangat kuat sehingga kita siap mengimpor perpustakaan besar dan berat untuk menggunakan fungsi kecil yang sebenarnya bisa kita tulis sendiri dengan mudah . Ini memperkenalkan ketergantungan yang tidak perlu dan kode yang bengkak. Buat keputusan yang terinformasi tentang kapan mengimpor perpustakaan eksternal dan kapan lebih tepat untuk menulis potongan kode singkat, mandiri untuk menyelesaikan tugas-tugas kecil.
Penyalahgunaan prinsip ini bisa menyebabkan ketergantungan tidak langsung (atau ketergantungan yang tidak perlu), abstraksi prematur dan solusi yang besar dan generic, kompleksitas pemeliharaan, beban kognitif yang tinggi.
9. Tight Coupling with a Framework
Frameworks berkembang dengan kecepatan mereka sendiri, yang dalam kebanyakan kasus tidak sejalan dengan lifecycle proyek kita.
Dengan terlalu bergantung pada sebuah framework, kita memaksa semua pengembang yang akan datang untuk belajar framework tersebut terlebih dahulu (atau versi tertentu). Meskipun framework memungkinkan kita meluncurkan MVP dalam hitungan hari, dalam jangka panjang mereka cenderung menambah kompleksitas dan beban kognitif yang tidak perlu.
Yang lebih buruk, pada suatu titik, framework dapat menjadi kendala yang signifikan ketika dihadapkan pada persyaratan baru yang tidak sesuai dengan arsitektur. Dari sini ke depan, orang-orang akhirnya membuat salinan (fork) dari sebuah framework dan menjaga versi kustom mereka sendiri. Bayangkan jumlah beban kognitif yang harus diatasi oleh seorang pendatang baru (yaitu, mempelajari framework kustom ini) agar bisa men-deliver value. 🤯
Bukan berarti kita menganjurkan untuk menciptakan segalanya dari awal!
Kita dapat menulis kode dengan cara yang agak independen dari framework. Logika bisnis seharusnya tidak berada di dalam sebuah framework; sebaliknya, seharusnya menggunakan komponen-komponen dari framework tersebut. Letakkan framework di luar logika inti kalian. Gunakan framework dengan cara yang mirip dengan library. Ini akan memungkinkan kontributor baru untuk menambah value dari hari pertama, tanpa perlu melalui puing-puing kompleksitas terkait framework terlebih dahulu.
10. Hexagonal/Onion Architecture
Ada kegembiraan seputar engineering tentang semua hal ini.
Penulis sendiri adalah pendukung bersemangat dari arsitektur onion selama bertahun-tahun. Penulis menggunakannya di sana-sini dan mendorong tim lain untuk melakukannya. Kompleksitas proyek-proyek kami meningkat, jumlah file saja sudah melonjak. Rasanya seperti kita menulis banyak kode penghubung. Pada persyaratan yang selalu berubah, kami harus membuat perubahan di berbagai lapisan abstraksi, semuanya menjadi merepotkan. 🤯
Melompat dari panggilan ke panggilan untuk membaca dan mencari tahu apa yang salah dan apa yang kurang adalah persyaratan vital untuk cepat menyelesaikan masalah. Dengan keterlepasan lapisan arsitektur ini, diperlukan faktor eksponensial dari jejak ekstra, sering kali terputus, untuk sampai pada titik di mana kegagalan terjadi. 🤯
Arsitektur ini pada awalnya membuat intuisi, tetapi setiap kali kami mencoba menerapkannya pada proyek, itu lebih banyak merugikan daripada memberikan keuntungan. Pada akhirnya, kami menyerah dan memilih prinsip dependency inversion yang baik. Tidak ada istilah port/adapter yang harus dipelajari, tidak ada lapisan abstraksi horizontal yang tidak perlu, tidak ada beban kognitif yang berlebihan.
Jangan tambahkan lapisan abstraksi hanya demi sebuah arsitektur. Tambahkan mereka setiap kali kalian membutuhkan titik ekstensi yang dibenarkan oleh alasan praktis. Lapisan abstraksi tidak gratis, mereka harus diingat dalam memori kerja kita.
Meskipun arsitektur onion ini telah mempercepat pergeseran penting dari aplikasi berbasis database tradisional ke pendekatan yang agak independen dari infrastruktur, di mana logika bisnis inti independen dari segala sesuatu yang eksternal, ide tersebut sama sekali tidak baru.
Arsitektur-arsitektur ini bukanlah fundamental, mereka hanyalah konsekuensi subjektif dan bias dari prinsip-prinsip yang lebih fundamental. Mengapa bergantung pada interpretasi subjektif tersebut? Ikuti prinsip-prinsip dasarnya: dependency inversion principle, isolation, single source of truth, true invariant, complexity, cognitive load dan information hiding.
11. DDD
Domain-Driven Design (DDD) memiliki beberapa poin bagus, meskipun seringkali disalahpahami. Orang-orang mengatakan “Kami menulis kode dalam DDD”, yang dimana ini agak aneh, karena DDD adalah tentang ruang masalah, bukan ruang solusi.
Bahasa yang ada dimana saja, domain, konteks terbatas, agregat, event storming semuanya berkaitan dengan ruang masalah. Mereka dimaksudkan untuk membantu kita memahami wawasan tentang domain dan mengekstrak batas-batasnya. DDD memungkinkan developer, ahli domain, dan pemangku kepentingan bisnis berkomunikasi secara efektif menggunakan bahasa tunggal dan terpadu. Alih-alih fokus pada aspek-aspek ruang masalah DDD ini, kita cenderung menekankan struktur folder tertentu, layanan, repositori, dan teknik ruang solusi lainnya.
Kemungkinan besar cara kita menginterpretasikan DDD kemungkinan akan menjadi unik dan subjektif. Dan jika kita membangun kode berdasarkan pemahaman ini, yaitu jika kita menciptakan banyak beban kognitif yang tidak perlu — pengembang di masa depan bisa jadi akan kesulitan. 🤯
12. Learning from the Giants
Lihatlah design priciple salah satu perusahaan teknologi terbesar:
- Clarity: Tujuan dan dasar pemikiran kode jelas bagi pembaca.
- Simplicity: Kode mencapai tujuannya dengan cara yang paling sederhana mungkin.
- Concision: Kode mudah untuk memahami detail-detail yang relevan, dan penamaan serta struktur membimbing pembaca melalui detail-detail tersebut.
- Maintainability: Kode mudah bagi seorang pengembang di masa depan untuk memodifikasinya dengan benar.
- Consistency: Kode konsisten dengan seluruh basis kode.
Apakah istilah mode tren yang baru ini sesuai dengan prinsip-prinsip tersebut? Atau apakah yang dilakukannya hanya menciptakan beban kognitif yang tidak perlu?
“One principle duct tape programmers understand well is that any kind of coding technique that’s even slightly complicated is going to doom your project. Duct tape programmers tend to avoid C++, templates, multiple inheritance, multithreading and a host of other technologies that are all totally reasonable, when you think long and hard about them, but are, honestly, just a little bit too hard for the human brain.”.
- Joel Spolsky
Kesimpulan
“Sifat yang rumit dan multiaspek dari beban kognitif dalam ranah pemahaman dan pemecahan masalah memerlukan pendekatan yang teliti dan strategis agar bisa menavigasi kompleksitas dan mengoptimalkan alokasi kapasitas mental”.
Apakah kalian merasakannya? Kesimpulan di atas sulit untuk dipahami. Kami baru saja membuat sebuah contoh dari cognitive load yang tidak perlu di pikiran kalian. Jangan lakukan ini pada rekan kerja kalian.
Kita sudah memiliki kompleksitas yang cukup dalam task yang kita lakukan, mengapa dibuat makin kompleks? Kita seharusnya mengurangi setiap cognitive load di luar beban intrinsik yang terkait dengan task kita.
“Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it”
- Brian Kernighan
Kode yang boring dan straightforward adalah pilihan yang tepat!
Terimakasih sudah membaca gais 🎉. Tulisan diatas sudah disunting di beberapa kalimat/section agar masuk akal saat dibaca menggunakan bahasa Indonesia (termasuk beberapa istilah di keep di bahasa aslinya karena jujur rada asing ketika diterjemahkan ke bahasa Indonesia).