Jangan Perbaiki Bug, Perbaiki Sistem

Grace Hopper menemukan bug komputer pertama yang sebenarnya, 1947

Bug sangat menyebalkan! Anda hanya jalan-jalan, mencoba menulis hal baru yang keren, dan kemudian seseorang muncul dan berkata, “Hei, ingat hal yang Anda tulis sebelumnya dan tidak memikirkan sama sekali lagi? Ini benar-benar rusak dan 100% pelanggan kami membencimu sekarang, bahkan Jennifer! "

Yang lebih buruk adalah ketika Anda memperbaiki bug, dan kemudian enam bulan kemudian seseorang berkata "hei saya pikir hal lain ini juga rusak" dan Anda menghabiskan banyak waktu untuk melihatnya dan pada dasarnya BUG SAMA SAAT AKU MEMPERBAIKI ANDA. Ini yang terburuk!

Untuk mencoba membuat hal-hal tidak menjadi yang terburuk, ketika saya menemukan bug saya pikir "kelas bug apa ini?" Apakah itu kesalahan logika? Masalah dengan modul Foo dengan asumsi ada yang salah tentang modul Bar? Salah ketik? Melewati parameter dengan urutan yang salah? Kesalahpahaman tentang persyaratan?

Beberapa kelas bug tersebut (persyaratannya terutama!) Cukup sulit untuk diperbaiki secara sistematis, tetapi ada banyak bug yang dapat dihindari.

Mari kita lihat beberapa teknik untuk ini!

Mengubah Sistem sehingga Bug Tidak Mungkin

Ini adalah satu-satunya orang pintar yang selalu membuat pernyataan bernas, tidak membantu!

"Ada dua cara membangun desain perangkat lunak: Salah satu caranya adalah membuatnya sangat sederhana sehingga jelas tidak ada kekurangan, dan cara lainnya adalah membuatnya sangat rumit sehingga tidak ada kekurangan yang jelas."
- Tony Hoare
"Jika Anda ingin pemrogram yang lebih efektif, Anda akan menemukan bahwa mereka seharusnya tidak membuang waktu untuk debugging, mereka seharusnya tidak memperkenalkan bug untuk memulai."
- Edgar Dijsktra

Ini selalu tujuannya, tetapi saya tentu tidak selalu sampai di sana, dan saya berpendapat bahwa Anda tidak harus selalu sampai di sana! Diberi pilihan antara "membuat perubahan 30 menit ini yang melakukan hal yang kita butuhkan tetapi membuat sistem kita kurang elegan" dan "menghabiskan satu bulan merancang ulang sistem kita sehingga sepertinya fitur itu direncanakan sejak awal", sangat sedikit orang yang memiliki mewah memilih yang terakhir. Pengiriman penting dan dapat berarti perbedaan antara bersaing dan tertinggal.

Namun demikian, penyederhanaan harus selalu dipertimbangkan, karena masalah yang berlawanan juga terjadi: Anda akhirnya ketinggalan karena sistem Anda sangat rumit sehingga membuat perubahan adalah proses yang penuh dan panjang.

Manfaat dari "kode di mana bug tidak mungkin" tampak cukup jelas, hal yang rumit adalah mencari tahu kapan itu mungkin! Ini adalah contoh di mana saya pikir memperbaiki sistem adalah panggilan yang tepat:

Kami memiliki jenis yang disebut Konteks yang menyimpan informasi cakupan-permintaan (seperti apa yang diajukan pengguna, apakah mereka diautentikasi, dll). Secara khusus, ada dua hal yang disebut Subject dan MessageType. MessageType adalah nama layanan yang dipanggil, dan Subjek ... rumit. Di salah satu aplikasi kami, itu sama dengan MessageType, tetapi di aplikasi lain digunakan untuk mencari tahu "apakah pesan itu baru saja saya terima tanggapan langsung atas sesuatu yang saya kirim, atau apakah itu siaran?" mekanisme yang kita miliki.

Ini menyebabkan bug di mana-mana, karena orang akan melihat Konteks dan berpikir "hmm yang mana yang benar untuk saya pedulikan" dan mereka kadang-kadang memilih Subjek ketika mereka bermaksud MessageType, dan kemudian kode mereka akan berfungsi tetapi hanya beberapa waktu saja. Kode yang berfungsi tetapi hanya sebagian waktu yang merupakan jenis kode terburuk!

Jadi kami menghapus Subjek, dan sekarang kami melacak informasi "apakah ini respons langsung terhadap sesuatu yang saya kirim" dengan menggunakan penutupan ketika kami berlangganan. Sekarang bug "bekas Subjek saat saya maksudkan MessageType" tidak mungkin, karena Subjek tidak lagi menjadi masalah!

Kelas "melewati beberapa argumen fungsi dalam urutan yang salah" juga cenderung menjadi kandidat yang tepat untuk perbaikan sistem-perubahan. Misalnya, kami memiliki banyak tempat yang kami lewati info pagination seperti (offset int, limit int) dan sulit untuk mengingat urutan mana yang mereka masuki, yang telah menyebabkan kami beberapa bug selama bertahun-tahun ketika orang-orang salah memesan. , jadi sekarang kita melewati mereka sebagai tipe yang berbeda dan masalahnya telah benar-benar hilang karena kompiler berteriak kepada kita jika kita salah melakukannya!

Menangkap Semua Versi Bug Dengan Analisis Statis

Analis statis adalah program yang memeriksa program Anda tanpa menjalankannya. Ternyata ada banyak jenis bug yang bisa ditangkap dengan cara ini!

Backend kami ditulis dalam Go, jadi ada beberapa opsi bagus yang bisa kita gunakan. Go vet adalah penganalisa statis berbasis Go yang paling umum, tetapi ada yang lain. Kami menjalankan Staticcheck, misalnya, dan Anda juga harus! Ketika kami memperkenalkannya, kami menemukan banyak masalah lama dalam kode kami, dan ketika kami menambahkannya ke pemeriksaan integrasi berkesinambungan pra-penggabungan kami, kami mencegah lebih banyak bug masuk ke kode di tempat pertama. Staticcheck dan dokter hewan sama-sama hebat, tujuan "nol false positive" mereka membuatnya sangat mudah untuk diperkenalkan, karena hal-hal yang perlu Anda perbaiki semuanya "membuat kode lebih baik" dan bukan "mengubah kode dengan cara tertentu untuk menenangkan analisis statis alat "atau" tambahkan banyak filter untuk mengabaikan positif palsu ini ".

Kami juga menjalankan pemeriksa ejaan untuk file string kami, goimport, pemeriksa ejaan terpisah untuk kode sumber kami, tidak bertukar, tidak bertobat, dan gosec. Digabungkan, pemeriksaan ini mencegah puluhan masalah setiap minggu - sebagian besar hal-hal bergaya rewel, tetapi juga beberapa bug nyata.

TIP PRO: waktu termudah untuk memperkenalkan analisa statis baru adalah sekarang! Menambahkannya ke proyek baru yang kecil mudah dan mencegah bug bertambah seiring bertambahnya jumlah, tetapi semakin besar basis kode Anda, semakin banyak masalah yang harus Anda selesaikan ketika mencoba memperkenalkan cek baru. Oculus CTO dan pemandu pemrograman John Carmack menulis sesuatu tentang ini ketika ia bereksperimen dengan penganalisa C ++, PVS-Studio, dan hal itu tentu saja benar bagi kami:

“Saya perhatikan bahwa setiap kali PVS-Studio diperbarui, ia menemukan sesuatu dalam basis kode kami dengan aturan baru. Ini sepertinya menyiratkan bahwa jika Anda memiliki basis kode yang cukup besar, setiap kelas kesalahan yang secara hukum legal mungkin ada di sana. Dalam sebuah proyek besar, kualitas kode sama statistiknya dengan sifat material fisik - cacat ada di semua tempat, Anda hanya bisa berharap untuk meminimalkan dampak yang ditimbulkan pada pengguna Anda. "

Itu tidak berarti bahwa jika Anda sudah memiliki basis kode besar Anda tidak dapat menambahkan alat analisis baru, itu hanya berarti jika Anda menunggu lebih lama itu hanya akan semakin sulit!

Menulis Analisis Statis Anda Sendiri

Analisis statis orang lain bagus karena sudah ditulis, tetapi opsi yang harus dipertimbangkan lebih banyak orang adalah menulis analisis hanya untuk basis kode Anda!

Izinkan saya menunjukkan contoh mengapa itu mungkin berguna - pelokalan string kami mendukung variabel templat untuk substitusi, dan tampilannya seperti ini:

str: = loc.T ("Halo, {{.first_name}}, selamat datang di League!", peta antarmuka [string] {} {"first_name": "Reilly"})

Itu akan dilokalkan untuk pengguna Prancis ke "Bonjour, Reilly, bienvenue à League!". Menggunakan variabel templat seperti ini adalah hal yang berguna untuk dilakukan karena memungkinkan penerjemah lebih banyak konteks daripada hanya memiliki% s di sana, yang mungkin berdampak pada bagaimana mereka menerjemahkan frasa, dan jika Anda memiliki beberapa parameter mereka tidak perlu muncul dalam urutan yang sama dalam semua bahasa.

Namun, antarmuka ini rentan terhadap bug! Ini bug yang saya temukan dalam kode kami beberapa minggu yang lalu:

str: = loc.T (“Pengingat: Anda memiliki pemesanan yang dikonfirmasi untuk {{.title}} pada {{.date}}.", memetakan antarmuka [string] {} {"waktu": judul, "tanggal": tm })

Parameter timehere harus berupa judul, yang merupakan nama janji temu yang telah dipesan pengguna.

Memperbaiki bug ini cukup mudah, dan kami dapat menambahkan tes bahwa permohonan yang satu ini sekarang benar, tetapi juga mudah untuk memperkenalkan bug jenis yang sama di tempat lain dalam kode. Alih-alih, mari perbaiki bug ini di mana-mana, selamanya!

Antarmuka T () ini cukup nyaman, jadi saya tidak ingin mengubahnya, tetapi kami dapat memperkenalkan penganalisa statis sederhana untuk mencari bug semacam ini. Jika Anda penasaran, kode lengkap untuk ini terlihat seperti ini (sangat banyak indentasi!), Tetapi di sini adalah ringkasan dari apa yang dilakukannya:

  1. Apakah kita memanggil fungsi bernama T ()?
  2. Jika demikian, apakah argumen pertama string literal?
  3. Jika demikian, apakah ini merupakan templat Go yang valid? (galat jika tidak)
  4. Jika demikian, apakah argumen berikutnya memetakan?
  5. Jika demikian, apakah kunci untuk peta tersebut cocok dengan parameter template apa pun di argumen pertama kami? (galat jika tidak)

Seluruhnya 41 baris kode, dan 15 baris itu hanya untuk keluar dari semua cek bersarang itu. Lumayan!

Sekarang, cek seperti ini tidak akan pernah menemukan jalannya menjadi sesuatu seperti "pergi dokter hewan", karena fungsi yang disebut T () dapat melakukan apa saja yang Turing-lengkap, dan saya yakin cek ini memiliki false positive pada benar-benar aneh contoh kode yang tidak saya pikirkan.

TAPI!

Kami tidak menulis, pergi dokter hewan! Kami sedang menulis sesuatu yang harus bekerja tepat pada satu basis kode: milik kami. Itu memungkinkan Anda membuat segala macam asumsi yang tidak bisa Anda buat dengan alat serba guna! Secara kebetulan, itu juga alasan mengapa analisator kami lainnya bukan open-source; itu tidak berguna untuk kode orang lain (saya memang menerbitkan https://github.com/reillywatson/enumcover, yang merupakan pemeriksaan yang lebih bermanfaat secara umum).

Setelah Anda memahami paket go / ast, jenis cek ini cukup cepat untuk ditulis. Hal di atas membutuhkan waktu satu jam dari awal hingga selesai, dan sekarang kami tidak pernah lagi memiliki bug seperti ini!

TIP PRO: ast.Print () adalah teman baik Anda di sini! Saya tidak pernah dapat mengingat semua node yang berbeda dan apa yang mereka sebut, tetapi saya tidak harus. Saya hanya menulis program sampel terkecil yang dapat saya pikirkan dan menjalankannya melalui ast.Print () untuk melihat seperti apa bentuk pohon parse, kemudian menulis fungsi analisis saya sehingga menangkap kode yang terlihat seperti apa pun jenis pohon parse program sampel itu. .

Berikut adalah beberapa jenis bug lain yang kami tangkap dengan cara yang sama:

  • memanggil fmt.Printf () dan teman-teman dalam kode produksi (kami memiliki paket logger internal yang ingin kami gunakan sebagai gantinya, ia menyediakan lebih banyak konteks untuk debugging dan menangani rotasi file log)
  • memanggil fungsi-fungsi tertentu yang menggunakan antarmuka {} tetapi argumen tersebut perlu menjadi sebuah pointer (ada dokter hewan check-in go yang mirip untuk panggilan Unmarshal, kami menggunakan teknik yang sama tetapi dengan daftar fungsi kami sendiri)

Versi Go selanjutnya, 1.12, mencakup beberapa dukungan untuk menjalankan analisis kustom tanpa biaya kinerja untuk menguraikan AST secara terpisah untuk setiap cek yang Anda tulis (lihat di sini untuk detail lebih lanjut). Kami belum menggunakannya (kami menjalankan harness yang dibangun di atas Staticcheck), tetapi kami berencana untuk memindahkannya segera setelah 1,12 dirilis.

Kesimpulan

Menulis perangkat lunak pada skala sulit, dan kecepatan perubahan hal-hal berarti bug akan terjadi! Namun, jika Anda dapat memperlakukan setiap bug dengan sikap "bagaimana ini terjadi dan bagaimana kami dapat memastikannya tidak terjadi lagi", setiap bug menjadi peluang untuk membuat semuanya lebih baik, alih-alih hanya membuat satu hal menjadi lebih buruk.

Jika Anda suka bekerja dengan orang pintar untuk menjadikan hal-hal yang lebih baik menjadi lebih baik, itu benar-benar sebuah pilihan, kami sedang merekrut!