Coba / Tangkap / Log / Rethrow - Apakah Anti Pola?

19

Saya dapat melihat beberapa pos di mana pentingnya menangani pengecualian di lokasi pusat atau pada batas proses ditekankan sebagai praktik yang baik daripada membuang sampah sembarangan setiap blok kode di sekitar try / catch. Saya sangat percaya bahwa sebagian besar dari kita memahami pentingnya hal itu, tetapi saya melihat orang-orang masih berakhir dengan pola anti tangkapan-log-rethrow terutama karena untuk memudahkan pemecahan masalah selama pengecualian apa pun, mereka ingin mencatat lebih banyak informasi spesifik konteks (misalnya: parameter metode berlalu) dan caranya adalah dengan membungkus metode sekitar try / catch / log / rethrow.

public static bool DoOperation(int num1, int num2)
{
    try
    {
        /* do some work with num1 and num2 */
    }
    catch (Exception ex)
    {
        logger.log("error occured while number 1 = {num1} and number 2 = {num2}"); 
        throw;
    }
}

Apakah ada cara yang benar untuk mencapai hal ini sambil tetap mempertahankan pengecualian dalam menangani praktik yang baik? Saya mendengar tentang kerangka kerja AOP seperti PostSharp untuk ini tetapi ingin tahu apakah ada kerugian atau biaya kinerja utama yang terkait dengan kerangka kerja AOP ini.

Terima kasih!

rahulaga_dev
sumber
6
Ada perbedaan besar antara membungkus setiap metode dalam try / catch, mencatat pengecualian dan membiarkan chug kode berjalan bersama. Dan coba / tangkap dan rethrowing pengecualian dengan informasi tambahan. Pertama adalah latihan yang mengerikan. Kedua adalah cara yang sangat baik untuk meningkatkan pengalaman debug Anda.
Euforia
Saya katakan coba / tangkap setiap metode dan cukup login di catch block dan rethrow - apakah ini boleh dilakukan?
rahulaga_dev
2
Seperti yang ditunjukkan oleh Amon. Jika bahasa Anda memiliki tumpukan-jejak, login di setiap tangkapan tidak ada gunanya. Tetapi membungkus pengecualian dan menambahkan informasi tambahan adalah praktik yang baik.
Euforia
1
Lihat jawaban @ Liath. Jawaban apa pun yang saya berikan akan sangat mencerminkan dirinya: menangkap pengecualian sedini mungkin dan jika yang dapat Anda lakukan pada tahap itu adalah mencatat beberapa informasi yang bermanfaat, maka lakukan dan rethrow. Melihat ini sebagai antipattern tidak masuk akal dalam pandangan saya.
David Arno
1
Liath: menambahkan cuplikan kode kecil. Saya menggunakan c #
rahulaga_dev

Jawaban:

19

Masalahnya bukan blok tangkap lokal, masalahnya adalah log dan rethrow . Baik menangani pengecualian atau membungkusnya dengan pengecualian baru yang menambahkan konteks tambahan dan membuangnya. Kalau tidak, Anda akan mengalami beberapa entri log duplikat untuk pengecualian yang sama.

Idenya di sini adalah untuk meningkatkan kemampuan untuk debug aplikasi Anda.

Contoh # 1: Tangani

try
{
    doSomething();
}
catch (Exception e)
{
    log.Info("Couldn't do something", e);
    doSomethingElse();
}

Jika Anda menangani pengecualian, Anda dapat dengan mudah menurunkan peringkat pentingnya entri log pengecualian dan tidak ada alasan untuk meresap pengecualian itu ke atas rantai. Sudah ditangani.

Menangani pengecualian dapat mencakup memberi tahu pengguna bahwa ada masalah, mencatat acara, atau mengabaikannya.

CATATAN: jika Anda sengaja mengabaikan pengecualian, saya sarankan memberikan komentar di klausa tangkapan kosong yang dengan jelas menunjukkan alasannya. Ini membuat pengelola di masa depan tahu bahwa itu bukan kesalahan atau pemrograman yang malas. Contoh:

try
{
    context.DrawLine(x1,y1, x2,y2);
}
catch (OutOfMemoryException)
{
    // WinForms throws OutOfMemory if the figure you are attempting to
    // draw takes up less than one pixel (true story)
}

Contoh # 2: Tambahkan konteks tambahan dan lempar

try
{
    doSomething(line);
}
catch (Exception e)
{
    throw new MyApplicationException(filename, line, e);
}

Menambahkan konteks tambahan (seperti nomor baris dan nama file dalam kode parsing) dapat membantu meningkatkan kemampuan untuk debug file input - dengan asumsi masalahnya ada di sana. Ini semacam kasus khusus, jadi balaslah pengecualian dalam "ApplicationException" hanya untuk mengubah citra itu tidak membantu Anda melakukan debug. Pastikan Anda menambahkan informasi tambahan.

Contoh # 3: Jangan melakukan apa pun dengan pengecualian

try
{
    doSomething();
}
finally
{
   // cleanup resources but let the exception percolate
}

Dalam kasus terakhir ini, Anda hanya membiarkan pengecualian pergi tanpa menyentuhnya. Handler pengecualian di lapisan terluar dapat menangani logging. The finallyklausa digunakan untuk memastikan sumber daya yang dibutuhkan oleh metode Anda dibersihkan, tapi ini bukan tempat untuk log bahwa pengecualian dilemparkan.

Berin Loritsch
sumber
Saya suka " Masalahnya bukan blok tangkap lokal, masalahnya adalah log dan rethrow " Saya pikir ini masuk akal untuk memastikan logging lebih bersih. Tetapi pada akhirnya itu juga berarti OK dengan mencoba / menangkap yang tersebar di semua metode, bukan? Saya pikir harus ada pedoman untuk memastikan praktik ini diikuti dengan bijaksana daripada setiap metode melakukannya.
rahulaga_dev
Saya memberikan panduan dalam jawaban saya. Apakah ini tidak menjawab pertanyaan Anda?
Berin Loritsch
@rahulaga_dev Saya tidak berpikir ada panduan / peluru perak jadi selesaikan masalah ini, karena sangat tergantung pada konteksnya. Tidak mungkin ada pedoman umum yang memberi tahu Anda di mana harus menangani pengecualian atau kapan harus menyusun kembali. IMO, satu-satunya pedoman yang saya lihat adalah menunda penebangan / penanganan hingga waktu yang memungkinkan dan untuk menghindari masuk dalam kode yang dapat digunakan kembali, sehingga Anda tidak membuat dependensi yang tidak perlu. Pengguna kode Anda tidak akan terlalu geli jika Anda mencatat sesuatu (yaitu pengecualian yang ditangani) tanpa memberi mereka kesempatan untuk menanganinya dengan cara mereka sendiri. Hanya dua sen saya :)
andreee
7

Saya tidak percaya tangkapan lokal adalah anti-pola, bahkan jika saya ingat benar itu benar-benar diberlakukan di Jawa!

Apa kunci saya ketika menerapkan penanganan kesalahan adalah strategi keseluruhan. Anda mungkin menginginkan filter yang menangkap semua pengecualian di batas layanan, Anda mungkin ingin mencegatnya secara manual - keduanya baik-baik saja selama ada strategi keseluruhan, yang akan masuk ke dalam standar pengkodean tim Anda.

Secara pribadi saya suka menangkap kesalahan di dalam suatu fungsi ketika saya dapat melakukan salah satu dari yang berikut:

  • Tambahkan informasi kontekstual (seperti keadaan objek atau apa yang sedang terjadi)
  • Tangani pengecualian dengan aman (seperti metode TryX)
  • Sistem Anda melewati batas layanan dan memanggil ke perpustakaan atau API eksternal
  • Anda ingin menangkap dan menyusun kembali jenis pengecualian yang berbeda (mungkin dengan yang asli sebagai pengecualian dalam)
  • Pengecualian dilemparkan sebagai bagian dari beberapa fungsi latar belakang bernilai rendah

Jika bukan salah satu dari kasus ini saya tidak menambahkan coba / tangkapan lokal. Jika ya, tergantung pada skenario saya dapat menangani pengecualian (misalnya metode TryX yang mengembalikan false) atau rethrow sehingga pengecualian akan ditangani oleh strategi global.

Sebagai contoh:

public bool TryConnectToDatabase()
{
  try
  {
    this.ConnectToDatabase(_databaseType); // this method will throw if it fails to connect
    return true;
  }
  catch(Exception ex)
  {
     this.Logger.Error(ex, "There was an error connecting to the database, the databaseType was {0}", _databaseType);
    return false;
  }
}

Atau contoh rethrow:

public IDbConnection ConnectToDatabase()
{
  try
  {
    // connect to the database and return the connection, will throw if the connection cannot be made
  }
  catch(Exception ex)
  {
     this.Logger.Error(ex, "There was an error connecting to the database, the databaseType was {0}", _databaseType);
    throw;
  }
}

Kemudian Anda menangkap kesalahan di bagian atas tumpukan dan menyajikan pesan ramah pengguna yang bagus kepada pengguna.

Apapun pendekatan yang Anda lakukan, selalu layak untuk membuat unit test untuk skenario ini sehingga Anda dapat memastikan fungsionalitasnya tidak berubah dan mengganggu aliran proyek di kemudian hari.

Anda belum menyebutkan bahasa tempat Anda bekerja, tetapi menjadi pengembang .NET dan terlalu sering melihat ini.

JANGAN MENULIS:

catch(Exception ex)
{
  throw ex;
}

Menggunakan:

catch(Exception ex)
{
  throw;
}

Yang pertama me-reset jejak stack dan membuat tangkapan level atas Anda sama sekali tidak berguna!

TLDR

Menangkap secara lokal bukanlah anti-pola, sering dapat menjadi bagian dari desain dan dapat membantu menambahkan konteks tambahan ke kesalahan.

Liath
sumber
3
Apa gunanya masuk dalam tangkapan, ketika penebang yang sama akan digunakan dalam pengendali pengecualian tingkat atas?
Euforia
Anda mungkin memiliki informasi tambahan (seperti variabel lokal) yang tidak akan Anda akses di bagian atas tumpukan. Saya akan memperbarui contoh untuk diilustrasikan.
Liath
2
Dalam hal itu, buang pengecualian baru dengan data tambahan dan pengecualian dalam.
Euforia
2
@ Euphoric ya, saya sudah melihat itu dilakukan juga, secara pribadi saya bukan penggemar namun karena mengharuskan Anda untuk membuat jenis pengecualian baru untuk hampir setiap metode / skenario yang saya rasa banyak overhead. Menambahkan baris log di sini (dan mungkin yang lain di atas) membantu menggambarkan aliran kode ketika mendiagnosis masalah
Liath
4
Java tidak memaksa Anda untuk menangani pengecualian, itu memaksa Anda untuk menyadarinya. Anda dapat menangkapnya dan melakukan apa pun atau hanya menyatakannya sebagai sesuatu yang dapat dilempar oleh fungsi dan tidak melakukan apa-apa dengan itu dalam fungsi .... Sedikit rewel pada jawaban yang cukup bagus!
Newtopian
4

Ini sangat tergantung pada bahasa. Misalnya C ++ tidak menawarkan jejak stack dalam pesan kesalahan pengecualian, jadi melacak pengecualian melalui catch-log-rethrow yang sering dapat membantu. Sebaliknya, Java dan bahasa serupa menawarkan jejak tumpukan yang sangat baik, meskipun format jejak tumpukan ini mungkin tidak terlalu dapat dikonfigurasi. Pengecualian penangkapan dan rethrowing dalam bahasa-bahasa ini sama sekali tidak ada artinya kecuali Anda benar-benar dapat menambahkan beberapa konteks penting (misalnya menghubungkan pengecualian SQL tingkat rendah dengan konteks operasi logika bisnis).

Setiap strategi penanganan kesalahan yang diterapkan melalui refleksi hampir selalu kurang efisien daripada fungsi yang dibangun ke dalam bahasa. Selain itu, logging yang meluas memiliki overhead kinerja yang tidak dapat dihindari. Jadi Anda perlu menyeimbangkan aliran informasi yang Anda dapatkan dengan persyaratan lain untuk perangkat lunak ini. Yang mengatakan, solusi seperti PostSharp yang dibangun pada instrumentasi tingkat kompiler umumnya akan melakukan jauh lebih baik daripada refleksi run-time.

Saya pribadi percaya bahwa mencatat semuanya tidak bermanfaat, karena memuat banyak informasi yang tidak relevan. Karena itu saya skeptis terhadap solusi otomatis. Dengan kerangka logging yang baik, mungkin cukup untuk memiliki pedoman pengkodean yang disepakati yang membahas jenis informasi apa yang ingin Anda catat dan bagaimana info ini harus diformat. Anda kemudian dapat menambahkan logging di tempat penting.

Masuk pada logika bisnis jauh lebih penting daripada masuk pada fungsi utilitas. Dan mengumpulkan jejak tumpukan laporan kerusakan dunia nyata (yang hanya membutuhkan penebangan di tingkat atas suatu proses) memungkinkan Anda untuk menemukan area kode tempat penebangan memiliki nilai paling besar.

amon
sumber
4

Ketika saya melihat try/catch/logdalam setiap metode itu menimbulkan kekhawatiran bahwa pengembang tidak tahu apa yang mungkin atau mungkin tidak terjadi dalam aplikasi mereka, dianggap yang terburuk, dan terlebih dahulu mencatat semuanya di mana-mana karena semua bug yang mereka harapkan.

Ini adalah gejala bahwa pengujian unit dan integrasi tidak cukup dan pengembang terbiasa untuk melangkah melalui banyak kode dalam debugger dan berharap bahwa entah bagaimana banyak logging akan memungkinkan mereka untuk menyebarkan kode buggy di lingkungan pengujian dan menemukan masalah dengan melihat log.

Kode yang melempar pengecualian bisa lebih berguna daripada kode redundan yang menangkap dan mencatat pengecualian. Jika Anda melemparkan pengecualian dengan pesan yang bermakna ketika suatu metode menerima argumen yang tidak terduga (dan mencatatnya di batas layanan) itu jauh lebih bermanfaat daripada langsung mencatat pengecualian yang dilemparkan sebagai efek samping dari argumen yang tidak valid dan harus menebak apa yang menyebabkannya. .

Nulls adalah contohnya. Jika Anda mendapatkan nilai sebagai argumen atau hasil dari pemanggilan metode dan itu tidak boleh nol, lempar pengecualian itu. Jangan hanya mencatat hasil yang NullReferenceExceptiondilemparkan lima baris kemudian karena nilai nol. Apa pun cara Anda mendapatkan pengecualian, tetapi yang satu memberi tahu Anda sesuatu sementara yang lain membuat Anda mencari sesuatu.

Seperti yang dikatakan oleh orang lain, yang terbaik adalah mencatat pengecualian di batas layanan, atau setiap kali pengecualian tidak ditata ulang karena ditangani dengan anggun. Perbedaan yang paling penting adalah antara sesuatu dan tidak sama sekali. Jika pengecualian Anda masuk di satu tempat yang mudah dijangkau, Anda akan menemukan informasi yang Anda butuhkan saat Anda membutuhkannya.

Scott Hannen
sumber
Terima kasih Scott. Poin yang Anda buat " Jika Anda melempar pengecualian dengan pesan yang bermakna ketika suatu metode menerima argumen yang tidak terduga (dan mencatatnya di batas layanan) " benar-benar menyerang dan membantu saya memvisualisasikan situasi yang melayang di sekitar saya dalam konteks argumen metode. Saya pikir masuk akal untuk memiliki klausa penjaga yang aman dan melempar ArgumentException dalam kasus itu daripada mengandalkan menangkap dan mencatat detail argumen
rahulaga_dev
Scott, aku punya perasaan yang sama. Setiap kali saya melihat log dan rethow hanya untuk mencatat konteks, saya merasa seperti pengembang tidak dapat mengontrol invarian kelas atau tidak dapat melindungi pemanggilan metode. Alih-alih setiap metode sepanjang jalan terbungkus dalam try / catch / log / throw yang serupa. Dan itu mengerikan.
Maks
2

Jika Anda perlu merekam informasi konteks yang belum ada dalam pengecualian, Anda membungkusnya dalam pengecualian baru, dan memberikan pengecualian asli sebagai InnerException. Dengan begitu Anda masih memiliki jejak stack asli yang dipertahankan. Begitu:

public static bool DoOperation(int num1, int num2)
{
    try
    {
        /* do some work with num1 and num2 */
    }
    catch (Exception ex)
    {
        throw new Exception("error occured while number 1 = {num1} and number 2 = {num2}", ex);
    }
}

Parameter kedua ke Exceptionkonstruktor memberikan pengecualian dalam. Kemudian Anda bisa mencatat semua pengecualian di satu tempat, dan Anda masih mendapatkan jejak tumpukan penuh dan informasi kontekstual dalam entri log yang sama.

Anda mungkin ingin menggunakan kelas pengecualian kustom, tetapi intinya sama.

coba / tangkap / log / rethrow berantakan karena akan menyebabkan log membingungkan - misalnya bagaimana jika pengecualian berbeda terjadi di utas lain di antara mencatat informasi kontekstual dan mencatat pengecualian aktual di dalam penangan tingkat atas? coba / tangkap / lempar baik-baik saja, jika pengecualian baru menambahkan informasi ke aslinya.

JacquesB
sumber
Bagaimana dengan jenis pengecualian asli? Jika kita bungkus, itu hilang. Apakah ini masalah? Misalnya, seseorang mengandalkan SqlTimeoutException.
Maks
@ Max: Jenis pengecualian asli masih tersedia sebagai pengecualian dalam.
JacquesB
Itu yang aku maksud! Sekarang semua orang di tumpukan panggilan, yang mengejar SqlException, tidak akan pernah mendapatkannya.
Maks
1

Pengecualian itu sendiri harus menyediakan semua info yang diperlukan untuk pencatatan yang benar, termasuk pesan, kode kesalahan, dan apa yang tidak. Karena itu seharusnya tidak perlu menangkap pengecualian hanya untuk rethrow atau melemparkan pengecualian yang berbeda.

Seringkali Anda akan melihat pola beberapa pengecualian yang ditangkap dan diubah kembali sebagai pengecualian umum, seperti menangkap DatabaseConnectionException, InvalidQueryException, dan InvalidSQLParameterException dan rethrowing DatabaseException. Meskipun untuk ini, saya berpendapat bahwa semua pengecualian khusus ini harus berasal dari DatabaseException di tempat pertama, jadi rethrowing tidak diperlukan.

Anda akan menemukan bahwa menghapus klausa catch catch yang tidak perlu (bahkan klausa untuk tujuan logging murni) sebenarnya akan membuat pekerjaan lebih mudah, bukan lebih sulit. Hanya tempat-tempat di program Anda yang menangani pengecualian yang harus mencatat pengecualian, dan, jika semuanya gagal, penangan pengecualian program untuk satu upaya terakhir untuk mencatat pengecualian sebelum keluar dari program dengan anggun. Pengecualian harus memiliki jejak tumpukan penuh yang menunjukkan titik tepat di mana pengecualian dilemparkan, sehingga seringkali tidak perlu menyediakan pencatatan "konteks".

Yang mengatakan AOP mungkin merupakan solusi perbaikan cepat untuk Anda, meskipun biasanya memerlukan sedikit perlambatan secara keseluruhan. Saya akan mendorong Anda untuk menghapus klausa catch catch yang tidak perlu sepenuhnya jika tidak ada nilai tambah yang ditambahkan.

Neil
sumber
1
" Pengecualian itu sendiri harus menyediakan semua info yang diperlukan untuk pencatatan yang benar, termasuk pesan, kode kesalahan, dan apa yang tidak " Mereka seharusnya, tetapi dalam praktiknya, mereka tidak menyebut pengecualian Null sebagai kasus klasik. Saya tidak mengetahui bahasa apa pun yang akan memberi tahu Anda variabel yang menyebabkannya di dalam ekspresi yang kompleks, misalnya.
David Arno
1
@ Davidvido Benar, tetapi konteks apa pun yang Anda berikan tidak bisa terlalu spesifik. Kalau tidak, Anda akan melakukannya try { tester.test(); } catch (NullPointerException e) { logger.error("Variable tester was null!"); }. Jejak tumpukan cukup dalam banyak kasus, tetapi kurang dari itu, jenis kesalahan biasanya memadai.
Neil