Lebih Baik Duplikasi daripada Abstraksi yang Salah (2016)
(sandimetz.com)- Duplikasi kode jauh lebih murah daripada abstraksi yang salah, dan generalisasi yang terlalu dini meningkatkan biaya pemeliharaan jangka panjang
- Bahkan ekstraksi yang awalnya masuk akal pun, ketika kebutuhan sedikit demi sedikit berubah, akan dipenuhi parameter dan pernyataan kondisional yang mengaburkan maksud awal
- Ketika satu abstraksi bersama mulai menanggung banyak ide, kode berubah menjadi prosedur yang berpusat pada kondisi, dan semakin banyak fitur ditambahkan, semakin mudah rusak
- Waspadai sunk cost fallacy yang membuat kita ingin mempertahankan usaha yang sudah dikeluarkan untuk kode lama; bila perlu, inline kembali abstraksi ke titik pemanggilan agar hanya kode yang benar-benar dibutuhkan yang tersisa
- Jika abstraksi yang salah sudah terlihat, lebih cepat untuk memperkenalkan kembali duplikasi, mengamati ulang kesamaan dalam kebutuhan saat ini, lalu mengekstraknya lagi setelah itu
Bagaimana abstraksi yang salah terbentuk
- Kalimat “duplication is far cheaper than the wrong abstraction” adalah bagian dari presentasi RailsConf 2014, dan terus sering dikutip setelahnya
- Jalur kegagalan yang umum adalah sebagai berikut
- Pengembang A menemukan duplikasi
- Duplikasi diekstrak menjadi metode atau kelas dan diberi nama untuk membuat abstraksi baru
- Kode berulang di titik pemanggilan diganti dengan pemanggilan abstraksi baru itu
- Seiring waktu, muncul kebutuhan baru yang hampir cocok tetapi tidak sepenuhnya sama
- Pengembang B, demi mempertahankan abstraksi yang ada, menambahkan parameter dan memasukkan kondisional yang menempuh jalur berbeda tergantung nilainya
- Setelah itu, tiap kebutuhan baru menambah parameter dan kondisional, sehingga kode makin sulit dipahami
- Kode yang sekali dibuat mudah terlihat seperti investasi yang harus dipertahankan
- Bekerja psikologi yang membuat kita merasa sayang pada usaha yang sudah dikeluarkan
- Semakin kompleks dan sulit dipahami suatu kode, semakin mudah kita merasa bahwa kode itu pasti penting dan memakan waktu lama, sehingga makin sulit untuk membuangnya
- Ini terkait dengan sunk cost fallacy
Kembali ke duplikasi lalu mengekstrak ulang
- Jika terus mengimplementasikan kebutuhan baru di atas abstraksi yang salah, kode bersama akan berubah menjadi berpusat pada kondisional, dan semakin banyak fitur ditambahkan, semakin tidak stabil
- Dalam situasi ini, jalan tercepat bukanlah terus memaksakannya, melainkan mundur sejenak
- Inline kembali kode yang sudah diabstraksikan ke setiap titik pemanggilan, lalu perkenalkan lagi duplikasi
- Berdasarkan parameter yang sebelumnya dikirim dari tiap titik pemanggilan, periksa hanya kode yang benar-benar dijalankan
- Hapus kode yang tidak diperlukan oleh titik pemanggilan tersebut
- Proses inline ini menghapus abstraksi dan kondisional sekaligus, lalu mereduksi setiap titik pemanggilan hingga hanya menyisakan kode yang memang dibutuhkannya
- Kode yang tampak seperti memanggil abstraksi yang sama pun, pada kenyataannya mungkin menjalankan jalur kode yang sangat unik di tiap titik pemanggilan
- Hanya setelah abstraksi lama benar-benar dihapus, kita bisa kembali mengamati duplikasi dan mengekstrak abstraksi baru yang sesuai dengan kebutuhan saat ini
- Jika parameter dan jalur kondisional terus ditambahkan ke kode bersama, besar kemungkinan abstraksi itu sudah tidak lagi tepat
- Pada awalnya, itu mungkin abstraksi yang tepat
- Namun perubahan kebutuhan bisa membuatnya sulit lagi dipertahankan dalam bentuk yang sama
- Dalam abstraksi yang salah, memperkenalkan kembali duplikasi bukanlah kemunduran, melainkan langkah maju yang lebih baik
5 komentar
Saya tidak yakin apakah ini topik yang memang perlu ditafsirkan secara dikotomis.
Oh, saya sangat setuju.
Yang belum tertata bisa dirapikan,
tapi membalik sesuatu yang sudah telanjur tertata sepertinya membutuhkan biaya yang jauh lebih besar.
ponytail sudah mengunggahnya, dan langsung muncul tulisan seperti ini wkwk
Selalu berhadap-hadapan.
Opini Hacker News
Menurut saya prinsip single source of truth harus selalu dijaga
Jika kode yang terduplikasi bisa menjadi bug saat saling berbeda, maka itu harus direfaktor. Kalau tidak, akan muncul long-range coupling yang sulit disadari pengembang di masa depan sampai bug benar-benar meledak
Namun, selama prinsip itu tidak dilanggar, abstraksi hanyalah soal kenyamanan; jika mulai terasa merepotkan, berarti ia tidak menjalankan perannya dan tidak ada alasan untuk memakainya. Jika sebuah fungsi memerlukan banyak flag untuk perilaku yang disesuaikan, besar kemungkinan itu adalah abstraksi yang keliru atau pelanggaran prinsip tanggung jawab tunggal
Jika benar-benar butuh banyak kustomisasi, sering kali lebih baik menerima fungsi/functor sebagai argumen. Misalnya, daripada
solve(f:double -> double, max_iters = 99, x_abs_tol = 1e-15, x_rel_tol = 1e-15, ...), bisa dibuat sepertisolve(f:double -> double, stopping_criteria: StoppingCriteriaClass)Tidak jelas apakah dua titik dalam kode memakai algoritme yang sama, atau versi yang sedikit berbeda, dan yang lebih penting, apakah keduanya akan berubah karena alasan yang sama
Petuah pada judul mengatakan bahwa memaksa hal-hal yang berbeda menjadi sama itu lebih menyakitkan daripada menduplikasi sesuatu yang sama lalu nanti membuatnya berbeda, dan saya setuju. Pada kasus kedua, kita hanya perlu melakukan perubahan yang sama dua kali atau merefaktor dengan memperkenalkan abstraksi, sedangkan pada kasus pertama kita harus terus menambal abstraksi itu atau membatalkannya
Ini terutama merusak locality, padahal saat melakukan perubahan, itulah sifat yang benar-benar penting. Saya hanya ingin melakukan perubahan ini saja tanpa harus khawatir akan efek samping pada bagian sistem yang tidak relevan
Contoh yang representatif adalah sinkronisasi pyproject.toml / requirements.txt, yang kadang memang merupakan pilihan terbaik, dan tampaknya ini bisa diterapkan lebih luas. Asumsinya, keadaan sudah cukup kacau sampai single source of truth tidak mungkin lagi, jadi ini lebih dekat ke pengurangan dampak daripada pengobatan
Saya sering mengalami situasi ketika dua potong kode tampak mirip pada satu waktu lalu diabstraksikan berlebihan, tetapi kemudian berkembang ke arah yang berbeda
Khususnya pengembang junior kadang memperlakukan duplikasi seolah-olah itu sumber dari segala kejahatan
Saya kadang memikirkan masalah ini. Baru-baru ini saya mengalaminya saat menangani sprite 2D untuk unit RTS di proyek pribadi, di mana sprite unit dimasukkan ke spritesheet dengan pola yang konsisten: 5 sprite untuk 8 arah, dengan 3 arah dimirror, dan urutannya stand, move, attack, die
Jadi saya membuat loader yang menerima action + direction dan mengembalikan array sprite yang harus diputar
Tetapi kemudian muncul sprite ledakan yang tidak punya arah, sprite mayat dengan 4 arah dan hanya 2 yang dimirror, lalu kasus di mana orc dan manusia sebagian besar berbagi sprite kecuali empat yang pertama
Saya sempat memikirkan apa sebenarnya abstraksi umum dari semua ini, tetapi akhirnya hanya memisahkan sebagian kode loading, membuat UnitLoader, CorpseLoader, dan EffectLoader, lalu lanjut saja. Mungkin ada abstraksi yang lebih baik karena ketiga loader itu menangani hal yang agak mirip, tetapi kalau nanti ketemu ya tidak masalah. Akan lebih mudah menghapus duplikasi nanti daripada sekarang membuat EverythingLoader yang rumit dan mencoba menangani semua kasus
Dalam pemrograman, ada naluri untuk menyederhanakan kode lewat generalisasi, tetapi kenyataannya berantakan sehingga sering kali kita malah terlalu menyederhanakan. Seperti dalam artikel ini, seiring waktu saat muncul kebutuhan baru, terlihat bahwa itu adalah penyederhanaan yang terlalu dini
“Abstraksi yang prematur adalah sumber dari banyak kekacauan” terdengar seperti petuah yang layak
Di lapisan di atasnya, yaitu penafsiran tata letak spritesheet dan penanganan mode pemutaran, ada berbagai variasi dan mungkin memang tidak ada abstraksi umum yang cocok untuk semua kasus
Daripada memaksa membuat abstraksi yang tidak terlihat atau menyesuaikan diri ke abstraksi yang tidak lengkap, saya lebih suka pendekatan seperti sekarang. Menunggu sampai abstraksinya benar-benar jelas dan kebutuhannya nyata adalah hal yang baik
Di sisi berlawanan dari DRY ada penawar bernama WET. Artinya tulis semuanya dua atau tiga kali. Yang lebih penting, menurut saya kita sebaiknya hanya mengabstraksikan kasus penggunaan yang benar-benar terbukti, biasanya yang pertama kali tampak sebagai duplikasi. Kode yang ditulis untuk kasus penggunaan masa depan yang belum ada sering justru menghalangi abstraksi atas hal yang benar-benar kita miliki sekarang, dan setiap kali itu terjadi rasanya lucu juga
Pekerjaan yang sulit dan membosankan bisa dilakukan saat sudah mencapai 10% terakhir dari proyek
Lagi pula, kadang “bug” yang lahir dari duplikasi justru menjadi fitur lucu yang disukai pemain
Dulu saat memakai OOP saya sering kesulitan karena abstraksi, tetapi setelah beralih ke pendekatan yang hampir murni fungsional, duplikasi kode jadi jarang terjadi
Tinggal buat fungsi lalu panggil dari dua tempat. Masalah abstraksi utama ada pada struktur data, tetapi interface TypeScript pada dasarnya adalah duck typing, jadi di sini pun masalahnya tidak terlalu besar
Karena itu, duplikasi kode yang disebabkan masalah abstraksi jarang terjadi. Duplikasi kode karena pengembang yang tersilo jauh lebih umum
Sebagian besar bahasa modern cukup mudah berdiri di atas teori pemrograman fungsional, jadi tidak perlu benar-benar menguasai Haskell. Mungkin cara berpikir orang berbeda-beda, tetapi bagi saya gagasan bahwa keseluruhan dibangun dari bagian-bagian kecil, sederhana, dan kadang fleksibel sangat cocok
Ini kebalikan dari mesin transformasi bentuk yang besar, rumit, dan melakukan segalanya
Begitu ukuran tim melewati titik tertentu sehingga tiap orang tidak bisa lagi tahu semua yang dikerjakan orang lain, duplikasi kode menjadi cukup tak terelakkan. Ini tetap berlaku meskipun semua orang menulis dengan gaya fungsional
Bahkan bulan lalu ini benar-benar terjadi di kantor. Saya menulis helper function baru yang murni dan menaruhnya di bagian awal file, lalu seminggu kemudian rekan memberi tahu bahwa helper yang secara substantif melakukan hal yang sama tetapi dengan signature berbeda sudah ada di bagian akhir file yang sama
Dalam konteks yang sama seperti teks utama, siapa pun yang pernah mengalami keduanya kemungkinan akan setuju. Codebase yang kurang didesain jauh lebih mudah ditangani daripada codebase yang didesain berlebihan
Kode terburuk yang pernah harus saya rawat adalah kode yang berusaha mengikuti DRY. Hanya saja, mereka tidak berusaha memahami maksud asli dari prinsip itu.
Satu-satunya cara keluar dari kekacauan itu adalah dengan memperkenalkan kembali duplikasi kode dalam cakupan yang luas
Di sini saya teringat dua presentasi: Mike Acton, Data-Oriented Design and C++ [1], dan Brian Cantrill, The Complexity of Simplicity [2]
Presentasi Mike mengatakan bahwa solusi kode tidak perlu memodelkan dunia nyata, data yang berbeda menciptakan masalah yang berbeda, dan karena itu membutuhkan solusi yang berbeda. Sulit menyampaikan presentasinya dengan cukup baik, tetapi itu sangat memengaruhi saya
Presentasi Brian membahas abstraksi secara umum dan betapa sulitnya menemukan abstraksi yang “benar”
Dulu, ketika saya baru beberapa tahun lulus sekolah, saya sedang mengimplementasikan connection pool di Rust, dan implementasi yang paling masuk akal adalah objek connection menyimpan weak reference ke pool agar otomatis dikembalikan saat di-
dropManajer saya, yang sangat berpengalaman, tidak suka ide ini karena “perpustakaan memegang buku, bukan buku yang memegang perpustakaan”. Saya tidak merasa itu alasan yang cukup meyakinkan untuk mengubah desain, tetapi dia tidak mau menangani masalah ini tanpa melihatnya melalui lensa metafora itu
Akhirnya kebuntuan terpecahkan ketika manajer lain mengusulkan, “buku perpustakaan memang tidak memuat perpustakaannya, tetapi ada cap nama perpustakaan di belakang yang menunjukkan ke tempat pengembalian”. Manajer itu tampaknya menganggap perluasan analogi ini masuk akal
Jika saat itu saya lebih berpengalaman, mungkin saya bisa menemukan cara berbicara di dalam metafora itu tanpa mengalah pada pokok persoalan, tetapi sampai sekarang pun rasanya benar-benar aneh bahwa dia memaksakan metafora itu sebagai kerangka standar alih-alih menimbang abstraksi kode dan konsekuensinya terhadap pengalaman penggunaan library
Tidak ada yang mau mendengarkan. Benar-benar tidak ada. Di 90% perusahaan selalu ada yang disebut senior developer yang begitu tergila-gila saat membuat abstraksi baru
Overengineering, abstraksi, dan optimisasi prematur adalah tiga bencana besar dalam engineering
Di saat yang sama, saya juga senang semuanya ada karena itu berarti pekerjaan akan selalu tersedia
Mirip dengan itu, beberapa developer tampaknya menganggap semua string inline atau konstanta angka sebagai kejahatan. Saya pernah melihat ini di sebuah PR
HTTPS_SCHEME = 'https'DOMAIN = 'www.example.com'url = HTTPS_SCHEME + '://' + DOMAINSaya tidak tahu apa manfaatnya selain mengikuti slogan “jangan hardcode konstanta” secara cargo cult. Apalagi definisi konstantanya ada di bagian paling atas file, sementara kode yang membangun URL ada ratusan baris jauhnya
Regex juga tidak perlu diletakkan di bagian paling atas file; letakkan saja di tempat ia dipakai. Bahasa pemrograman cukup pintar dan mungkin bisa mengetahui bahwa itu sebuah konstanta
Jika fungsinya sangat kecil, pakai saja lambda. Saya berharap orang tidak membuat fungsi satu baris yang hanya dipakai sekali atau dua kali di tempat yang sangat jauh
Jika pada lingkungan test atau staging Anda perlu mengganti https menjadi http, maka memisahkan skema dan domain serta menaruh konstanta di bagian atas atau di file terpisah memang masuk akal. Penting juga apakah
urldibangun di banyak tempat atau hanya di satu tempatMenaruh konstanta bernama di bagian atas file adalah gaya yang sangat umum, dan kadang memang menjadi bagian dari standar coding tim
Bisa juga ada alasan lain, jadi ada baiknya mengingat Chesterton’s Fence. Bagaimanapun juga, langsung menyimpulkan itu sebagai cargo cult bukan ide yang bagus. Seseorang juga bisa bilang memakai literal inline itu sendiri sama-sama cargo cult. Jika terlihat aneh, tanyakan saja; mungkin ada alasan bagus, atau mungkin tidak ada yang terlalu peduli dan mereka malah senang jika Anda merapikannya dengan meng-inline konstanta itu
Kalau itu diekstrak menjadi konstanta, Anda harus membuka proyek satu per satu lagi untuk mencari penggunaan
Kalau memakai microservice, Anda bisa melakukan keduanya
Jika Anda adalah maintainer satu service, tidak ada alasan untuk peduli pada kode yang ada di service lain. Itu kode tim lain, kenapa harus peduli? Anda bahkan tidak perlu tahu bahwa tim itu ada. Dalam sistem besar, sering kali memang tidak realistis untuk mengetahui keberadaan semua aplikasi
Hanya dengan $19.95, kami akan mengubah satu single point of failure menjadi banyak single point of failure!
Lebih baik memakai arsitektur berorientasi layanan tetapi tetap men-deploy monolit saja. Pengujiannya lebih mudah, dan Anda juga bisa menghindari lapisan tambahan berupa serialisasi/deserialisasi
Kebanyakan engineer senior tampaknya tahu bahwa DRY tidak boleh diikuti secara membabi buta. Meski begitu, banyak dari kita tetap merasa tidak nyaman dengan gagasan harus memelihara beberapa sumber kode yang duplikat
Untuk menanganinya, kita perlu mencermati model sederhana di mana dua pemanggil bergantung pada kode bersama. Jika kode bersama harus diubah karena kebutuhan hanya dari satu pemanggil, maka kode itu bukan bagian yang benar-benar bersama
Sasaran DRY yang keliru adalah mencoba menyelesaikannya lewat enkapsulasi. Enkapsulasi memindahkan pekerjaan refaktorisasi dari pemanggil ke kode bersama. Namun karena dampak dari memperbarui kode bersama jauh lebih besar daripada di sisi pemanggil, itu bukan arah yang diinginkan
Kita bisa tetap mematuhi DRY sambil menghindari enkapsulasi. Lebih baik memiliki beberapa abstraksi tipis yang perlu dipahami oleh pemanggil. Dalam OOP, ini dipelajari lewat SRP dan IoC, sedangkan dalam pemrograman prosedural hal ini muncul secara alami dalam bentuk rangkaian pemanggilan fungsi helper