"Seberapa buruk" kode yang tidak terkait dalam blok coba-tangkap-akhirnya?


11

Ini adalah T terkait: Apakah penggunaan klausa akhirnya untuk melakukan pekerjaan setelah kembali gaya buruk / berbahaya?

Dalam Q yang direferensikan, kode akhirnya terkait dengan struktur yang digunakan dan perlunya pre-fetching. Pertanyaan saya sedikit berbeda, dan saya yakin itu cocok untuk audiens yang lebih luas. Contoh khusus saya adalah aplikasi C # winform, tetapi ini akan berlaku untuk C ++ / Java akhirnya digunakan juga.

Saya memperhatikan beberapa blok coba-tangkap-akhirnya di mana ada banyak kode yang tidak terkait dengan pengecualian dan penanganan / pembersihan terkubur di dalam blok. Dan saya akan mengakui bias saya terhadap memiliki blok try-catch-akhirnya yang sangat ketat dengan kode yang terkait erat dengan pengecualian dan penanganannya. Berikut adalah beberapa contoh dari apa yang saya lihat.

Blok Try akan memiliki banyak panggilan awal dan variabel yang diatur mengarah ke kode yang bisa melempar. Informasi pencatatan akan mendapatkan pengaturan dan berjalan di blok coba juga.

Akhirnya blok akan memiliki panggilan form / modul / control form (meskipun aplikasi akan segera berakhir, seperti yang dinyatakan dalam catch block), serta membuat objek baru seperti panel.

Kurang lebih:

    methodName (...)
    {
        mencoba
        {
            // Banyak kode untuk metode ini ...
            // kode yang bisa melempar ...
            // Banyak lagi kode untuk metode dan pengembalian ...
        }
        menangkap (sesuatu)
        {// menangani pengecualian}
        akhirnya
        {
            // beberapa pembersihan karena pengecualian, menutup beberapa hal
            // lebih banyak kode untuk hal-hal yang telah dibuat (mengabaikan segala pengecualian yang bisa dilemparkan) ...
            // mungkin membuat beberapa objek lagi
        }
    }

Kode berfungsi, jadi ada beberapa nilai untuk itu. Tidak dienkapsulasi dengan baik dan logikanya agak berbelit-belit. Saya (dengan susah payah) akrab dengan risiko dalam mengubah kode di sekitar serta refactoring, jadi pertanyaan saya bermuara pada keinginan untuk mengetahui pengalaman orang lain dengan kode yang terstruktur serupa.

Apakah gaya buruk membenarkan membuat perubahan? Adakah yang terbakar parah dari situasi yang sama? Maukah Anda berbagi rincian pengalaman buruk itu? Biarkan itu karena saya bereaksi berlebihan dan itu tidak seburuk gaya? Dapatkan manfaat perawatan untuk membereskan semuanya?


Tag "C ++" tidak termasuk di sini, karena C ++ tidak memiliki (dan tidak perlu) finally. Semua kegunaan baik tercakup dalam RAII / RRID / SBRM (mana pun yang Anda suka).
David Thornley

2
@ Davidvidhorn: Selain dari contoh di mana kata kunci 'akhirnya' digunakan, sisa pertanyaan berlaku dengan baik untuk C ++. Saya adalah pengembang C ++ dan kata kunci itu tidak membingungkan saya. Dan mengingat saya memiliki percakapan serupa dengan tim saya sendiri secara semi-reguler, pertanyaan ini sangat relevan dengan apa yang kita lakukan dengan C ++
DXM

@DXM: Saya mengalami kesulitan memvisualisasikan ini (dan saya tahu apa yang finallyada di C #). Apa yang setara dengan C ++? Apa yang saya pikirkan adalah kode setelah catch, dan itu berlaku sama untuk kode setelah C # finally.
David Thornley

@ Davidvidhorn: kode pada akhirnya adalah kode yang dieksekusi tidak peduli apa .
orlp

1
@ nightcracker, itu tidak sepenuhnya benar. Itu tidak akan dieksekusi jika Anda melakukannya Environment.FailFast(); itu tidak dapat dieksekusi jika Anda memiliki pengecualian yang tidak tertangkap. Dan itu menjadi lebih rumit jika Anda memiliki blok iterator dengan finallyyang Anda iterate secara manual.
svick

Jawaban:


10

Saya telah melalui situasi yang sangat mirip ketika saya harus berurusan dengan kode Windows Forms warisan yang mengerikan yang ditulis oleh pengembang yang jelas tidak tahu apa yang mereka lakukan.

Pertama-tama, Anda tidak bertindak berlebihan. Ini kode yang salah. Seperti yang Anda katakan, blok tangkap harus tentang menggugurkan dan bersiap untuk berhenti. Ini bukan saatnya untuk membuat objek (khususnya Panel). Saya bahkan tidak bisa mulai menjelaskan mengapa ini buruk.

Yang telah dibilang...

Saran pertama saya adalah: jika tidak rusak, jangan menyentuhnya!

Jika pekerjaan Anda adalah mempertahankan kode, Anda harus melakukan yang terbaik untuk tidak merusaknya. Saya tahu ini menyakitkan (saya pernah ke sana) tetapi Anda harus melakukan yang terbaik untuk tidak merusak apa yang sudah bekerja.

Saran kedua saya adalah: jika Anda harus menambahkan lebih banyak fitur, cobalah menjaga struktur kode yang ada sebanyak mungkin agar Anda tidak melanggar kode.

Contoh: jika ada pernyataan kasus sakelar mengerikan yang Anda rasa dapat diganti dengan warisan yang tepat, Anda harus berhati-hati dan berpikir dua kali sebelum memutuskan untuk mulai memindahkan barang-barang.

Anda pasti akan menemukan situasi di mana refactoring adalah pendekatan yang tepat tetapi berhati-hatilah: kode refactoring lebih mungkin untuk memperkenalkan bug . Anda harus membuat keputusan itu dari perspektif pemilik aplikasi, bukan dari perspektif pengembang. Jadi Anda harus berpikir apakah usaha (uang) yang diperlukan untuk memperbaiki masalah itu layak dilakukan refactoring atau tidak. Saya telah melihat berkali-kali seorang pengembang menghabiskan beberapa hari memperbaiki sesuatu yang tidak benar-benar rusak hanya karena dia berpikir "kode itu jelek".

Saran ketiga saya adalah: Anda akan terbakar jika Anda memecahkan kode, tidak masalah apakah itu salah Anda atau tidak.

Jika Anda dipekerjakan untuk memberikan perawatan, tidak masalah jika aplikasi berantakan karena orang lain membuat keputusan yang buruk. Dari perspektif pengguna itu berfungsi sebelum dan sekarang Anda memecahkannya. Kamu memecahkannya!

Joel menempatkan dengan sangat baik dalam artikelnya menjelaskan beberapa alasan mengapa Anda tidak boleh menulis ulang kode warisan.

http://www.joelonsoftware.com/articles/fog0000000069.html

Jadi Anda harus merasa sangat buruk tentang kode semacam itu (dan Anda seharusnya tidak pernah menulis hal seperti itu) tetapi mempertahankannya adalah monster yang sama sekali berbeda.

Tentang pengalaman saya: Saya harus menjaga kode selama sekitar 1 tahun dan akhirnya saya bisa menulis ulang dari awal tetapi tidak sekaligus.

Apa yang terjadi adalah kode itu sangat buruk sehingga fitur-fitur baru tidak dapat diimplementasikan. Aplikasi yang ada memiliki masalah kinerja dan kegunaan yang serius. Akhirnya saya diminta untuk membuat perubahan yang akan memakan waktu 3-4 bulan (kebanyakan karena bekerja dengan kode itu membutuhkan waktu lebih lama dari biasanya). Saya pikir saya bisa menulis ulang seluruh bagian itu (termasuk mengimplementasikan fitur baru yang diinginkan) dalam waktu sekitar 5-6 bulan. Saya membawa proposisi ini kepada para pemangku kepentingan dan mereka setuju untuk menulis ulang (untungnya bagi saya).

Setelah saya menulis ulang karya ini, mereka mengerti bahwa saya bisa mengirimkan barang-barang yang jauh lebih baik daripada yang sudah mereka miliki. Jadi saya bisa menulis ulang seluruh aplikasi.

Pendekatan saya adalah menulis ulang sepotong demi sepotong. Pertama saya mengganti seluruh UI (Windows Forms), kemudian saya mulai mengganti lapisan komunikasi (panggilan Layanan Web) dan terakhir saya mengganti seluruh implementasi Server (itu adalah jenis aplikasi klien / server tebal).

Beberapa tahun kemudian dan aplikasi ini telah berubah menjadi alat kunci yang indah yang digunakan oleh seluruh perusahaan. Saya 100% yakin itu tidak akan mungkin terjadi seandainya saya tidak menulis ulang semuanya.

Meskipun saya bisa melakukannya, bagian yang penting adalah para pemangku kepentingan menyetujuinya dan saya bisa meyakinkan mereka bahwa itu sepadan dengan uang yang dikeluarkan. Jadi, sementara Anda harus mempertahankan aplikasi yang sudah ada, lakukan yang terbaik untuk tidak merusak apa pun dan jika Anda dapat meyakinkan pemilik tentang manfaat menulis ulang, maka cobalah melakukannya seperti Jack the Ripper: demi bagian.


1
+1. Tapi kalimat terakhir mengacu pada pembunuh berantai. Apakah itu benar-benar perlu?
MarkJ

Saya suka bagian dari ini tetapi pada saat yang sama meninggalkan desain yang rusak di tempat dan menambahkan barang sering mengarah ke desain yang lebih buruk. Lebih baik untuk mengetahui cara memperbaikinya dan memperbaikinya, meskipun dalam pendekatan yang terukur tentu saja.
Ricky Clarkson

@ RickyClarkson saya setuju. Sangat sulit untuk mengetahui kapan Anda melakukannya secara berlebihan (refactoring tidak benar-benar diperlukan) dan ketika Anda benar-benar melukai aplikasi dengan menambahkan perubahan.
Alex

@ RickyClarkson Saya sangat setuju dengan hal itu sehingga saya bahkan dapat menulis ulang proyek yang saya ikuti. Tetapi untuk waktu yang lama saya harus hati-hati menambahkan hal-hal yang berusaha menyebabkan kerusakan seminimal mungkin. Kecuali permintaannya adalah untuk memperbaiki sesuatu yang penyebab utamanya adalah keputusan desain yang buruk (yang biasanya tidak demikian), saya akan mengatakan bahwa desain aplikasi tidak boleh disentuh.
Alex

@RickyClarkson Itu adalah bagian dari pengalaman komunitas yang ingin saya manfaatkan. Mendeklarasikan semua kode lama sebagai buruk adalah anti-pola yang sama buruknya. Namun, ada hal-hal dalam kode ini yang sedang saya kerjakan yang benar-benar tidak boleh dilakukan sebagai bagian dari blok akhirnya. Umpan balik dan respons Alex adalah apa yang ingin saya baca.

2

Jika tidak perlu mengubah kode, biarkan apa adanya. Tetapi jika Anda harus mengubah kode karena Anda harus memperbaiki bug atau mengubah beberapa fungsi, Anda harus mengubahnya, apakah Anda suka atau tidak. IMHO seringkali lebih aman dan lebih rentan kesalahan, jika Anda refactor pertama menjadi potongan-potongan kecil, lebih baik dimengerti dan kemudian menambahkan fungsionalitas. Ketika Anda memiliki alat refactoring otomatis, ini dapat dilakukan relatif aman, dengan risiko yang sangat kecil untuk memperkenalkan bug baru. Dan saya sarankan Anda harus mendapatkan buku Michael Feathers

http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052

yang akan memberi Anda petunjuk berharga bagaimana membuat kode lebih dapat diuji, menambahkan tes unit dan menghindari melanggar kode saat menambahkan fitur baru.

Jika Anda melakukannya dengan benar, metode Anda semoga menjadi cukup sederhana, coba-coba-akhirnya tidak akan mengandung kode yang tidak terkait di tempat yang salah lagi.


2

Misalkan Anda memiliki enam metode untuk memanggil, empat di antaranya hanya boleh dipanggil jika yang pertama selesai tanpa kesalahan. Pertimbangkan dua alternatif:

try {
     method1();  // throws
     method2();
     method3();
     method4();
     method5();
 } catch(e) {
     // handle error
 }
 method6();

vs.

 try {
      method1();  // throws
 }
 catch(e) {
      // handle error
 }
 if(! error ) {
     method2();
     method3();
     method4();
     method5();
 }
 method6();

Di kode kedua, Anda memperlakukan pengecualian seperti kode pengembalian. Secara konseptual, ini identik dengan:

rc = method1();
if( rc != error ) {
     method2();
     method3();
     method4();
     method5();
 }
 method6();

Jika kita akan membungkus setiap metode yang dapat melempar pengecualian dalam blok coba / tangkap, apa gunanya memiliki pengecualian dalam fitur bahasa sama sekali? Tidak ada manfaatnya dan ada lagi mengetik.

Alasan mengapa kami memiliki pengecualian dalam bahasa adalah karena pada masa lalu kode pengembalian yang buruk, kami sering mengalami situasi di atas di mana kami memiliki beberapa metode yang dapat mengembalikan kesalahan, dan setiap kesalahan berarti membatalkan semua metode sepenuhnya. Di awal karir saya, di masa C dulu, saya melihat kode seperti ini di semua tempat:

rc = method1();
if( rc != error ) {
     rc = method2();
     if( rc != error ) {
         rc = method3();
         if( rc != error ) {
             rc = method4();
             if(rc != error ) {
                 method5();
             }
         }
     }
 }
 method6();

Ini adalah pola yang sangat umum, dan pengecualian yang diselesaikan dengan sangat rapi memungkinkan Anda untuk kembali ke contoh pertama di atas. Aturan gaya yang mengatakan setiap metode memiliki blok pengecualian sendiri benar-benar membuang ini ke luar jendela. Lalu mengapa harus repot?

Anda harus selalu berusaha untuk membuat blok coba melampaui set kode yang secara konseptual merupakan unit. Ini harus mengelilingi kode yang jika ada kesalahan dalam unit kode, maka tidak ada gunanya menjalankan kode lain di unit kode.

Kembali ke tujuan perkecualian: ingat bahwa kode itu dibuat karena sering kali kode tidak dapat dengan mudah menangani kesalahan saat terjadi. Inti dari pengecualian adalah untuk memungkinkan Anda menangani kesalahan secara signifikan dalam kode, atau lebih jauh menumpuk. Orang-orang lupa bahwa dan dalam banyak kasus menangkap mereka secara lokal ketika mereka akan lebih baik hanya mendokumentasikan bahwa metode tersebut dapat membuang pengecualian ini. Ada terlalu banyak kode di dunia yang terlihat seperti ini:

void method0() : throws MyNewException 
{
    try {
        method1();  // throws MyOtherException
    }
    catch(e) {
        if(e == MyOtherException)
            throw MyNewException();
    }
    method2();
}

Di sini, Anda hanya menambahkan lapisan, dan lapisan menambahkan kebingungan. Lakukan ini:

void method0() : throws MyOtherException
{
    method1();
    method2();
}

Selain itu, perasaan saya adalah bahwa jika Anda mengikuti aturan itu, dan menemukan diri Anda dengan lebih dari pasangan mencoba / menangkap blok dalam metode ini, Anda harus mempertanyakan apakah metode itu sendiri terlalu kompleks dan perlu dipecah menjadi beberapa metode .

Takeaway: blok try harus berisi set kode yang harus dibatalkan jika terjadi kesalahan di mana saja di blok. Apakah kumpulan kode ini satu baris atau seribu baris tidak masalah. (Meskipun jika Anda memiliki seribu baris dalam suatu metode, Anda punya masalah lain.)


Steven - terima kasih atas respons yang ditata dengan baik, dan saya sepenuhnya setuju dengan pikiran Anda. Saya tidak punya masalah dengan kode tipe yang terkait atau berurutan yang tinggal di blok percobaan tunggal. Seandainya saya bisa memposting potongan kode yang sebenarnya, itu akan menunjukkan 5 - 10 panggilan yang sama sekali tidak terkait sebelum panggilan pertama yang bisa dibuang. Satu blok akhirnya jauh lebih buruk - beberapa thread baru sedang diputar dan tambahan, rutinitas non-pembersihan dipanggil. Dan dalam hal ini, semua panggilan di blok itu akhirnya tidak ada hubungannya dengan apa pun yang bisa dilemparkan.

Salah satu masalah utama dengan pendekatan ini adalah bahwa tidak membuat perbedaan antara kasus-kasus di mana method0mungkin kuasi-berharap bahwa pengecualian mungkin dilemparkan, dan mereka yang tidak melakukannya. Sebagai contoh, misalkan method1kadang-kadang bisa melempar InvalidArgumentException, tetapi jika tidak, itu akan menempatkan objek ke keadaan sementara tidak valid yang akan diperbaiki oleh method2, yang diharapkan untuk selalu menempatkan objek kembali ke keadaan yang valid. Jika method2 melempar InvalidArgumentException, kode yang mengharapkan untuk menangkap pengecualian seperti itu method1akan menangkapnya, meskipun ...
supercat

... status sistem akan sesuai dengan harapan kode. Jika pengecualian yang dilemparkan dari method1 harus ditangani secara berbeda dari yang dilemparkan oleh method2, bagaimana hal itu bisa dilakukan tanpa membungkusnya?
supercat

Idealnya, pengecualian Anda cukup terperinci sehingga ini adalah situasi yang jarang terjadi.
Gort the Robot
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.