Kode Bersih dan Seni Penanganan Pengecualian
Diterbitkan: 2022-03-11Pengecualian sama tuanya dengan pemrograman itu sendiri. Kembali pada hari-hari ketika pemrograman dilakukan dalam perangkat keras, atau melalui bahasa pemrograman tingkat rendah, pengecualian digunakan untuk mengubah aliran program, dan untuk menghindari kegagalan perangkat keras. Hari ini, Wikipedia mendefinisikan pengecualian sebagai:
kondisi anomali atau luar biasa yang memerlukan pemrosesan khusus – sering kali mengubah aliran normal eksekusi program…
Dan penanganannya membutuhkan:
konstruksi bahasa pemrograman khusus atau mekanisme perangkat keras komputer.
Jadi, pengecualian memerlukan perlakuan khusus, dan pengecualian yang tidak ditangani dapat menyebabkan perilaku yang tidak terduga. Hasilnya seringkali spektakuler. Pada tahun 1996, kegagalan peluncuran roket Ariane 5 yang terkenal dikaitkan dengan pengecualian overflow yang tidak tertangani. Bug Perangkat Lunak Terburuk Sejarah berisi beberapa bug lain yang dapat dikaitkan dengan pengecualian yang tidak tertangani atau salah penanganan.
Seiring waktu, kesalahan ini, dan kesalahan lainnya yang tak terhitung jumlahnya (yang, mungkin, tidak sedramatis itu, tetapi masih menjadi bencana besar bagi mereka yang terlibat) berkontribusi pada kesan bahwa pengecualian itu buruk .
Tetapi pengecualian adalah elemen mendasar dari pemrograman modern; mereka ada untuk membuat perangkat lunak kami lebih baik. Daripada takut akan pengecualian, kita harus merangkulnya dan belajar bagaimana memanfaatkannya. Pada artikel ini, kita akan membahas bagaimana mengelola pengecualian secara elegan, dan menggunakannya untuk menulis kode bersih yang lebih mudah dipelihara.
Penanganan Pengecualian: Ini Hal yang Baik
Dengan munculnya pemrograman berorientasi objek (OOP), dukungan pengecualian telah menjadi elemen penting dari bahasa pemrograman modern. Saat ini, sistem penanganan eksepsi yang kuat telah dibangun ke dalam sebagian besar bahasa. Misalnya, Ruby menyediakan pola tipikal berikut:
begin do_something_that_might_not_work! rescue SpecificError => e do_some_specific_error_clean_up retry if some_condition_met? ensure this_will_always_be_executed end
Tidak ada yang salah dengan kode sebelumnya. Tetapi penggunaan pola ini secara berlebihan akan menyebabkan bau kode, dan belum tentu bermanfaat. Demikian juga, menyalahgunakannya sebenarnya dapat membahayakan basis kode Anda, membuatnya rapuh, atau mengaburkan penyebab kesalahan.
Stigma seputar pengecualian sering membuat programmer merasa bingung. Adalah fakta kehidupan bahwa pengecualian tidak dapat dihindari, tetapi kita sering diajarkan bahwa mereka harus ditangani dengan cepat dan tegas. Seperti yang akan kita lihat, ini belum tentu benar. Sebaliknya, kita harus mempelajari seni menangani pengecualian dengan anggun, membuatnya harmonis dengan kode kita yang lain.
Berikut adalah beberapa praktik yang direkomendasikan yang akan membantu Anda menerima pengecualian dan memanfaatkannya serta kemampuannya untuk menjaga agar kode Anda dapat dipelihara , diperluas , dan dibaca :
- rawatan : Memungkinkan kita untuk dengan mudah menemukan dan memperbaiki bug baru, tanpa takut merusak fungsionalitas saat ini, memperkenalkan bug lebih lanjut, atau harus meninggalkan kode sama sekali karena meningkatnya kompleksitas dari waktu ke waktu.
- extensibility : Memungkinkan kita untuk dengan mudah menambahkan ke basis kode kita, menerapkan persyaratan baru atau yang diubah tanpa merusak fungsionalitas yang ada. Ekstensibilitas memberikan fleksibilitas, dan memungkinkan penggunaan ulang tingkat tinggi untuk basis kode kami.
- keterbacaan : Memungkinkan kita membaca kode dengan mudah dan menemukan tujuannya tanpa menghabiskan terlalu banyak waktu untuk menggali. Ini sangat penting untuk menemukan bug dan kode yang belum diuji secara efisien.
Elemen-elemen ini adalah faktor utama dari apa yang kita sebut kebersihan atau kualitas , yang bukan merupakan ukuran langsung itu sendiri, tetapi merupakan efek gabungan dari poin-poin sebelumnya, seperti yang ditunjukkan dalam komik ini:
Dengan itu, mari selami praktik ini dan lihat bagaimana masing-masing dari mereka memengaruhi ketiga tindakan tersebut.
Catatan: Kami akan menyajikan contoh dari Ruby, tetapi semua konstruksi yang ditunjukkan di sini memiliki padanan dalam bahasa OOP yang paling umum.
Selalu buat hierarki ApplicationError
Anda sendiri
Sebagian besar bahasa datang dengan berbagai kelas pengecualian, diatur dalam hierarki pewarisan, seperti kelas OOP lainnya. Untuk menjaga keterbacaan, pemeliharaan, dan ekstensibilitas kode kita, ada baiknya untuk membuat subpohon pengecualian khusus aplikasi kita sendiri yang memperluas kelas pengecualian dasar. Menginvestasikan waktu dalam menyusun hierarki ini secara logis bisa sangat bermanfaat. Sebagai contoh:
class ApplicationError < StandardError; end # Validation Errors class ValidationError < ApplicationError; end class RequiredFieldError < ValidationError; end class UniqueFieldError < ValidationError; end # HTTP 4XX Response Errors class ResponseError < ApplicationError; end class BadRequestError < ResponseError; end class UnauthorizedError < ResponseError; end # ...
Memiliki paket pengecualian komprehensif yang dapat diperluas untuk aplikasi kita membuat penanganan situasi khusus aplikasi ini menjadi lebih mudah. Misalnya, kita dapat memutuskan pengecualian mana yang harus ditangani dengan cara yang lebih alami. Ini tidak hanya meningkatkan keterbacaan kode kami, tetapi juga meningkatkan pemeliharaan aplikasi dan perpustakaan kami (permata).
Dari perspektif keterbacaan, jauh lebih mudah dibaca:
rescue ValidationError => e
Daripada membaca:
rescue RequiredFieldError, UniqueFieldError, ... => e
Dari perspektif pemeliharaan, katakanlah, misalnya, kami mengimplementasikan API JSON, dan kami telah mendefinisikan ClientError
kami sendiri dengan beberapa subtipe, untuk digunakan ketika klien mengirim permintaan yang buruk. Jika salah satu dari ini dimunculkan, aplikasi harus membuat representasi JSON dari kesalahan dalam responsnya. Akan lebih mudah untuk memperbaiki, atau menambahkan logika, ke satu blok yang menangani ClientError
s daripada mengulang setiap kesalahan klien yang mungkin dan menerapkan kode penangan yang sama untuk masing-masing. Dari segi ekstensibilitas, jika nanti kami harus menerapkan jenis kesalahan klien lain, kami percaya itu akan ditangani dengan baik di sini.
Selain itu, ini tidak mencegah kami menerapkan penanganan khusus tambahan untuk kesalahan klien tertentu sebelumnya di tumpukan panggilan, atau mengubah objek pengecualian yang sama di sepanjang jalan:
# app/controller/pseudo_controller.rb def authenticate_user! fail AuthenticationError if token_invalid? || token_expired? User.find_by(authentication_token: token) rescue AuthenticationError => e report_suspicious_activity if token_invalid? raise e end def show authenticate_user! show_private_stuff!(params[:id]) rescue ClientError => e render_error(e) end
Seperti yang Anda lihat, menaikkan pengecualian khusus ini tidak mencegah kami untuk dapat menanganinya pada tingkat yang berbeda, mengubahnya, menaikkannya kembali, dan mengizinkan pengendali kelas induk untuk menyelesaikannya.
Dua hal yang perlu diperhatikan di sini:
- Tidak semua bahasa mendukung memunculkan pengecualian dari dalam penangan pengecualian.
- Di sebagian besar bahasa, memunculkan pengecualian baru dari dalam penangan akan menyebabkan pengecualian asli hilang selamanya, jadi lebih baik untuk menaikkan kembali objek pengecualian yang sama (seperti dalam contoh di atas) untuk menghindari kehilangan jejak penyebab asli dari kesalahan. (Kecuali Anda melakukan ini dengan sengaja).
Jangan pernah rescue Exception
Artinya, jangan pernah mencoba menerapkan penangan catch-all untuk tipe pengecualian dasar. Menyelamatkan atau menangkap semua pengecualian grosir tidak pernah merupakan ide yang baik dalam bahasa apa pun, baik itu secara global pada tingkat aplikasi dasar, atau dalam metode terkubur kecil yang hanya digunakan sekali. Kami tidak ingin menyelamatkan Exception
karena itu akan mengaburkan apa pun yang sebenarnya terjadi, merusak kemampuan pemeliharaan dan ekstensibilitas. Kita dapat membuang banyak waktu untuk men-debug apa masalah sebenarnya, ketika itu bisa sesederhana kesalahan sintaks:
# main.rb def bad_example i_might_raise_exception! rescue Exception nah_i_will_always_be_here_for_you end # elsewhere.rb def i_might_raise_exception! retrun do_a_lot_of_work! end
Anda mungkin telah memperhatikan kesalahan pada contoh sebelumnya; return
salah ketik. Meskipun editor modern memberikan beberapa perlindungan terhadap jenis kesalahan sintaksis khusus ini, contoh ini menggambarkan bagaimana rescue Exception
merusak kode kita. Tidak ada jenis pengecualian yang sebenarnya (dalam hal ini NoMethodError
) yang ditangani, juga tidak pernah diekspos ke pengembang, yang dapat menyebabkan kita membuang banyak waktu untuk berputar-putar.
Jangan pernah rescue
lebih banyak pengecualian daripada yang Anda butuhkan
Poin sebelumnya adalah kasus khusus dari aturan ini: Kita harus selalu berhati-hati untuk tidak menggeneralisasi penangan pengecualian kita secara berlebihan. Alasannya sama; setiap kali kami menyelamatkan lebih banyak pengecualian daripada yang seharusnya, kami akhirnya menyembunyikan bagian dari logika aplikasi dari tingkat aplikasi yang lebih tinggi, belum lagi menekan kemampuan pengembang untuk menangani pengecualian itu sendiri. Ini sangat mempengaruhi ekstensibilitas dan pemeliharaan kode.
Jika kami mencoba untuk menangani subtipe pengecualian yang berbeda di penangan yang sama, kami memperkenalkan blok kode gemuk yang memiliki terlalu banyak tanggung jawab. Misalnya, jika kita membuat pustaka yang menggunakan API jarak jauh, menangani MethodNotAllowedError
(HTTP 405), biasanya berbeda dengan menangani UnauthorizedError
(HTTP 401), meskipun keduanya adalah ResponseError
s.
Seperti yang akan kita lihat, seringkali ada bagian berbeda dari aplikasi yang akan lebih cocok untuk menangani pengecualian tertentu dengan cara yang lebih KERING.

Jadi, tentukan tanggung jawab tunggal dari kelas atau metode Anda, dan tangani pengecualian minimal yang memenuhi persyaratan tanggung jawab ini . Misalnya, jika suatu metode bertanggung jawab untuk mendapatkan info stok dari API jarak jauh, maka metode tersebut harus menangani pengecualian yang muncul dari mendapatkan info itu saja, dan menyerahkan penanganan kesalahan lainnya ke metode berbeda yang dirancang khusus untuk tanggung jawab ini:
def get_info begin response = HTTP.get(STOCKS_URL + "#{@symbol}/info") fail AuthenticationError if response.code == 401 fail StockNotFoundError, @symbol if response.code == 404 return JSON.parse response.body rescue JSON::ParserError retry end end
Di sini kami mendefinisikan kontrak untuk metode ini agar kami hanya mendapatkan info tentang stok. Ini menangani kesalahan khusus titik akhir , seperti respons JSON yang tidak lengkap atau salah format. Itu tidak menangani kasus ketika otentikasi gagal atau kedaluwarsa, atau jika stok tidak ada. Ini adalah tanggung jawab orang lain, dan secara eksplisit dilewatkan ke tumpukan panggilan di mana seharusnya ada tempat yang lebih baik untuk menangani kesalahan ini dengan cara KERING.
Tahan keinginan untuk segera menangani pengecualian
Ini adalah pelengkap poin terakhir. Pengecualian dapat ditangani pada titik mana pun dalam tumpukan panggilan, dan titik mana pun dalam hierarki kelas, jadi mengetahui dengan tepat di mana menanganinya dapat membingungkan. Untuk memecahkan teka-teki ini, banyak pengembang memilih untuk menangani pengecualian apa pun segera setelah muncul, tetapi menginvestasikan waktu untuk memikirkan hal ini biasanya akan menghasilkan menemukan tempat yang lebih tepat untuk menangani pengecualian tertentu.
Salah satu pola umum yang kita lihat di aplikasi Rails (terutama yang mengekspos API khusus JSON) adalah metode pengontrol berikut:
# app/controllers/client_controller.rb def create @client = Client.new(params[:client]) if @client.save render json: @client else render json: @client.errors end end
(Perhatikan bahwa meskipun ini secara teknis bukan penangan pengecualian, secara fungsional, ini melayani tujuan yang sama, karena @client.save
hanya mengembalikan false ketika menemukan pengecualian.)
Namun, dalam kasus ini, mengulangi pengendali kesalahan yang sama di setiap tindakan pengontrol adalah kebalikan dari KERING, dan merusak kemampuan pemeliharaan dan ekstensibilitas. Sebagai gantinya, kita dapat menggunakan sifat khusus dari propagasi pengecualian, dan menanganinya hanya sekali, di kelas pengontrol induk, ApplicationController
:
# app/controllers/client_controller.rb def create @client = Client.create!(params[:client]) render json: @client end
# app/controller/application_controller.rb rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity def render_unprocessable_entity(e) render \ json: { errors: e.record.errors }, status: 422 end
Dengan cara ini, kami dapat memastikan bahwa semua kesalahan ActiveRecord::RecordInvalid
dengan benar dan DRY-ly di satu tempat, pada level ApplicationController
dasar. Ini memberi kita kebebasan untuk bermain-main dengan mereka jika kita ingin menangani kasus-kasus tertentu di tingkat yang lebih rendah, atau membiarkannya menyebar dengan anggun.
Tidak semua pengecualian perlu penanganan
Saat mengembangkan permata atau perpustakaan, banyak pengembang akan mencoba merangkum fungsionalitas dan tidak mengizinkan pengecualian apa pun untuk menyebar keluar dari perpustakaan. Namun terkadang, tidak jelas bagaimana menangani pengecualian hingga aplikasi tertentu diimplementasikan.
Mari kita ambil ActiveRecord
sebagai contoh solusi ideal. Perpustakaan menyediakan pengembang dengan dua pendekatan untuk kelengkapan. Metode save
menangani pengecualian tanpa menyebarkannya, cukup mengembalikan false
, sambil save!
menimbulkan pengecualian ketika gagal. Ini memberi pengembang opsi untuk menangani kasus kesalahan tertentu secara berbeda, atau hanya menangani kegagalan apa pun secara umum.
Tetapi bagaimana jika Anda tidak memiliki waktu atau sumber daya untuk menyediakan implementasi yang lengkap? Dalam hal ini, jika ada ketidakpastian, yang terbaik adalah mengekspos pengecualian , dan melepaskannya ke alam liar.
Inilah alasannya: Kami bekerja dengan persyaratan pemindahan hampir sepanjang waktu, dan membuat keputusan bahwa pengecualian akan selalu ditangani dengan cara tertentu dapat benar-benar membahayakan implementasi kami, merusak ekstensibilitas dan pemeliharaan, dan berpotensi menambah utang teknis yang besar, terutama saat mengembangkan perpustakaan.
Ambil contoh sebelumnya dari konsumen API saham yang mengambil harga saham. Kami memilih untuk menangani respons yang tidak lengkap dan salah format di tempat, dan kami memilih untuk mencoba lagi permintaan yang sama hingga kami mendapat respons yang valid. Namun nanti, persyaratannya mungkin berubah, sehingga kami harus kembali ke data stok historis yang disimpan, alih-alih mencoba lagi permintaan tersebut.
Pada titik ini, kita akan dipaksa untuk mengubah perpustakaan itu sendiri, memperbarui bagaimana pengecualian ini ditangani, karena proyek dependen tidak akan menangani pengecualian ini. (Bagaimana mereka bisa? Itu tidak pernah diekspos kepada mereka sebelumnya.) Kami juga harus memberi tahu pemilik proyek yang mengandalkan perpustakaan kami. Ini mungkin menjadi mimpi buruk jika ada banyak proyek seperti itu, karena kemungkinan besar mereka dibangun dengan asumsi bahwa kesalahan ini akan ditangani dengan cara tertentu.
Sekarang, kita dapat melihat ke mana arah tujuan kita dengan manajemen dependensi. Pandangannya tidak bagus. Situasi ini cukup sering terjadi, dan lebih sering daripada tidak, itu menurunkan kegunaan, ekstensibilitas, dan fleksibilitas perpustakaan.
Jadi, inilah intinya: jika tidak jelas bagaimana pengecualian harus ditangani, biarkan ia menyebar dengan anggun . Ada banyak kasus di mana ada tempat yang jelas untuk menangani pengecualian secara internal, tetapi ada banyak kasus lain di mana mengekspos pengecualian lebih baik. Jadi sebelum Anda memilih untuk menangani pengecualian, pikirkan saja. Aturan praktis yang baik adalah hanya bersikeras menangani pengecualian saat Anda berinteraksi langsung dengan pengguna akhir.
Ikuti konvensi
Implementasi Ruby, dan, terlebih lagi, Rails, mengikuti beberapa konvensi penamaan, seperti membedakan antara method_names
dan method_names!
dengan "ledakan." Di Ruby, bang menunjukkan bahwa metode akan mengubah objek yang memanggilnya, dan di Rails, itu berarti metode akan memunculkan pengecualian jika gagal menjalankan perilaku yang diharapkan. Cobalah untuk menghormati konvensi yang sama, terutama jika Anda akan membuka perpustakaan Anda.
Jika kita menulis method!
dengan ledakan di aplikasi Rails, kita harus mempertimbangkan konvensi ini. Tidak ada yang memaksa kami untuk mengajukan pengecualian ketika metode ini gagal, tetapi dengan menyimpang dari konvensi, metode ini dapat menyesatkan programmer untuk percaya bahwa mereka akan diberi kesempatan untuk menangani pengecualian sendiri, padahal sebenarnya tidak.
Konvensi Ruby lain, yang dikaitkan dengan Jim Weirich, adalah menggunakan fail
untuk menunjukkan kegagalan metode, dan hanya menggunakan raise
jika Anda menaikkan kembali pengecualian.
Selain itu, karena saya menggunakan pengecualian untuk menunjukkan kegagalan, saya hampir selalu menggunakan kata kunci
fail
daripada kata kunciraise
di Ruby. Fail dan raise adalah sinonim sehingga tidak ada perbedaan kecuali bahwa fail lebih jelas mengomunikasikan bahwa metode tersebut telah gagal. Satu-satunya waktu saya menggunakan kenaikan gaji adalah ketika saya menangkap pengecualian dan menaikkannya kembali, karena di sini saya tidak gagal, tetapi secara eksplisit dan sengaja menaikkan pengecualian. Ini adalah masalah gaya yang saya ikuti, tetapi saya ragu banyak orang lain melakukannya.
Banyak komunitas bahasa lain telah mengadopsi konvensi seperti ini seputar bagaimana pengecualian diperlakukan, dan mengabaikan konvensi ini akan merusak keterbacaan dan pemeliharaan kode kita.
Logger.log(semuanya)
Praktik ini tidak hanya berlaku untuk pengecualian, tentu saja, tetapi jika ada satu hal yang harus selalu dicatat, itu adalah pengecualian.
Logging sangat penting (cukup penting bagi Ruby untuk mengirimkan logger dengan versi standarnya). Ini adalah buku harian aplikasi kami, dan bahkan lebih penting daripada mencatat bagaimana aplikasi kami berhasil, adalah mencatat bagaimana dan kapan mereka gagal.
Tidak ada kekurangan perpustakaan logging atau layanan berbasis log dan pola desain. Sangat penting untuk melacak pengecualian kami sehingga kami dapat meninjau apa yang terjadi dan menyelidiki jika ada sesuatu yang tidak beres. Pesan log yang tepat dapat mengarahkan pengembang langsung ke penyebab masalah, sehingga menghemat waktu mereka.
Keyakinan Kode Bersih itu
Pengecualian adalah bagian mendasar dari setiap bahasa pemrograman. Mereka istimewa dan sangat kuat, dan kita harus memanfaatkan kekuatan mereka untuk meningkatkan kualitas kode kita alih-alih melelahkan diri kita sendiri untuk bertarung dengan mereka.
Dalam artikel ini, kami mempelajari beberapa praktik yang baik untuk menyusun pohon pengecualian kami dan bagaimana hal itu dapat bermanfaat bagi keterbacaan dan kualitas untuk menyusunnya secara logis. Kami melihat pendekatan yang berbeda untuk menangani pengecualian, baik di satu tempat atau di beberapa tingkatan.
Kami melihat bahwa itu buruk untuk "menangkap mereka semua", dan tidak apa-apa untuk membiarkan mereka mengapung dan menggelembung.
Kami melihat di mana menangani pengecualian dengan cara KERING, dan mengetahui bahwa kami tidak berkewajiban untuk menanganinya kapan atau di mana mereka pertama kali muncul.
Kami membahas kapan tepatnya adalah ide yang baik untuk menangani mereka, kapan itu ide yang buruk, dan mengapa, jika ragu, adalah ide yang baik untuk membiarkan mereka menyebar.
Terakhir, kami membahas poin lain yang dapat membantu memaksimalkan kegunaan pengecualian, seperti mengikuti konvensi dan mencatat semuanya.
Dengan pedoman dasar ini, kita bisa merasa jauh lebih nyaman dan percaya diri menangani kasus kesalahan dalam kode kita, dan membuat pengecualian kita benar-benar luar biasa!
Terima kasih khusus kepada Avdi Grimm dan pembicaraannya yang luar biasa, Exceptional Ruby, yang banyak membantu dalam pembuatan artikel ini.