7 poin oleh GN⁺ 5 jam lalu | 5 komentar | Bagikan ke WhatsApp
  • 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

 
dieafterwork 1 jam lalu

Saya tidak yakin apakah ini topik yang memang perlu ditafsirkan secara dikotomis.

 
hanje3765 2 jam lalu

Oh, saya sangat setuju.
Yang belum tertata bisa dirapikan,
tapi membalik sesuatu yang sudah telanjur tertata sepertinya membutuhkan biaya yang jauh lebih besar.

 
jimmy2056 3 jam lalu

ponytail sudah mengunggahnya, dan langsung muncul tulisan seperti ini wkwk

 
shakespeares 4 jam lalu

Selalu berhadap-hadapan.

 
GN⁺ 5 jam lalu
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 seperti solve(f:double -> double, stopping_criteria: StoppingCriteriaClass)

    • Inti tulisan ini adalah membahas kasus ketika belum jelas ada berapa sumber kebenaran
      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
    • Jika karena tekanan ekstrem perangkat lunak sudah terlanjur terdorong ke dua sumber kebenaran, menambahkan tes CI yang mencegah merge ke main saat kedua sumber tidak cocok bisa cukup efektif
      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
    • Standar “kalau berbeda maka jadi bug” adalah aturan praktis yang sangat bagus
      Saya sering mengalami situasi ketika dua potong kode tampak mirip pada satu waktu lalu diabstraksikan berlebihan, tetapi kemudian berkembang ke arah yang berbeda
    • Secara teori ini benar, tetapi di dunia nyata banyak orang berusaha menghindari duplikasi apa pun tanpa kecuali
      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

    • Saya suka kutipan, “Segala sesuatu harus dibuat sesederhana mungkin, tetapi jangan lebih sederhana dari itu”
      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
    • Sangat mungkin abstraksi umumnya sebenarnya sudah terpisah. Yaitu kode untuk memuat dan menampilkan piksel dari satu sprite
      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
    • Pendekatan ini benar. Membuat game seharusnya memang menyenangkan
      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

    • Saya memakai bahasa fungsional sebagai hobi, dan menurut saya inti yang perlu diingat adalah tekniknya
      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
    • Untuk mengalami duplikasi kode, pengembang tidak harus benar-benar tersilo
      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
    • Saya penasaran apa tepatnya yang dimaksud dengan “memanggil fungsi dari dua bagian”
  • 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

    • Tenang saja, tidak apa-apa, tinggal tambahkan beberapa parameter boolean yang ambigu ke fungsi reusable itu agar mendukung use case baru lalu deploy
    • Intinya adalah mereka “sudah mencoba”. Mereka melakukan itu untuk sementara waktu sampai tiba di titik ketika abstraksinya salah dan tidak bisa lagi diikuti dengan setia
  • 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”

    1. https://www.youtube.com/watch?v=rX0ItVEVjHc
    2. https://www.youtube.com/watch?v=Cum5uN2634o
    • Saya selalu merasa aneh ketika bahkan engineer yang cukup pintar memprioritaskan metafora dunia nyata dibanding kebutuhan nyata codebase
      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-drop
      Manajer 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

    • Kubernetes, microservice yang jumlahnya lebih banyak daripada engineer, protokol rumit demi menghemat beberapa byte overhead, semuanya cloud, dan segudang class yang sebenarnya bisa berupa fungsi sederhana, semuanya contoh yang tepat
  • 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 + '://' + DOMAIN
    Saya 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

    • Dalam kode, saya sangat menyukai kedekatan. Saya lebih suka sesuatu didefinisikan sedekat mungkin dengan tempat penggunaannya. Ini kebiasaan yang benar-benar mengganggu
      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
    • Menaruh konstanta di bagian atas memang bisa membuatnya lebih mudah dikustomisasi. Terutama jika file ini nantinya diduplikasi
      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 url dibangun di banyak tempat atau hanya di satu tempat
      Menaruh 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
    • Saya juga pernah mengalami hal seperti ini. Jika Event punya nama, Anda bisa langsung grep di seluruh monolit besar atau kumpulan repository microservice untuk menemukan semua file yang berkaitan dengan event itu
      Kalau itu diekstrak menjadi konstanta, Anda harus membuka proyek satu per satu lagi untuk mencari penggunaan
  • Kalau memakai microservice, Anda bisa melakukan keduanya

    • Saya tahu ini bercanda, tetapi dalam dunia microservice yang ideal, tidak ada konsep duplikasi kode antarservice
      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
    • Tunggu dulu! Masih ada lagi!
      Hanya dengan $19.95, kami akan mengubah satu single point of failure menjadi banyak single point of failure!
    • Sembilan dari sepuluh kali, microservice akhirnya menjadi sangat saling bergantung dan berubah menjadi monolit terdistribusi
      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