Apa cara yang baik untuk menyeimbangkan pengecualian informatif dan kode bersih?


11

Dengan SDK publik kami, kami cenderung ingin memberikan pesan yang sangat informatif tentang mengapa pengecualian terjadi. Sebagai contoh:

if (interfaceInstance == null)
{
     string errMsg = string.Format(
          "Construction of Action Argument: {0}, via the empty constructor worked, but type: {1} could not be cast to type {2}.",
          ParameterInfo.Name,
          ParameterInfo.ParameterType,
          typeof(IParameter)
    );

    throw new InvalidOperationException(errMsg);
}

Namun, ini cenderung mengacaukan aliran kode, karena cenderung lebih fokus pada pesan kesalahan daripada apa yang dilakukan kode.

Seorang kolega mulai refactoring beberapa pengecualian untuk sesuatu seperti ini:

if (interfaceInstance == null)
    throw EmptyConstructor();

...

private Exception EmptyConstructor()
{
    string errMsg = string.Format(
          "Construction of Action Argument: {0}, via the empty constructor worked, but type: {1} could not be cast to type {2}.",
          ParameterInfo.Name,
          ParameterInfo.ParameterType,
          typeof(IParameter)
    );

    return new InvalidOperationException(errMsg);
}

Yang membuat logika kode lebih mudah dipahami, tetapi menambahkan banyak metode tambahan untuk melakukan penanganan kesalahan.

Apa cara lain untuk menghindari masalah "logika kekacauan lama-pesan-pengecualian"? Saya terutama bertanya tentang idiom C # /. NET, tetapi bagaimana bahasa lain mengaturnya juga membantu.

[Sunting]

Akan menyenangkan untuk memiliki pro dan kontra dari setiap pendekatan juga.


4
IMHO solusi kolega Anda adalah yang sangat bagus, dan saya kira jika Anda benar-benar mendapatkan banyak metode tambahan semacam itu, Anda dapat menggunakan kembali setidaknya beberapa dari mereka. Memiliki banyak metode kecil tidak masalah ketika metode Anda dinamai dengan baik, asalkan mereka membuat blok bangunan yang mudah dipahami dari program Anda - ini tampaknya menjadi masalah di sini.
Doc Brown

@DocBrown - Ya, saya suka ide (ditambahkan sebagai jawaban di bawah ini, bersama dengan pro / kontra), tetapi untuk kedua intellisense dan untuk sejumlah metode yang potensial, mulai terlihat seperti kekacauan juga.
FriendlyGuy

1
Pikir: Jangan jadikan pesan sebagai pembawa semua detail pengecualian. Kombinasi menggunakan Exception.Dataproperti, "pilih-pilih" menangkap menangkap, memanggil kode menangkap & menambahkan konteksnya sendiri, bersama dengan tumpukan panggilan yang ditangkap semua berkontribusi informasi yang harus memungkinkan pesan jauh lebih sedikit. Akhirnya System.Reflection.MethodBaseterlihat menjanjikan untuk memberikan rincian untuk diteruskan ke metode "pengecualian konstruksi" Anda.
radarbob

@radarbob Anda menyarankan bahwa mungkin pengecualiannya terlalu bertele-tele? Mungkin kita membuatnya terlalu seperti logging.
FriendlyGuy

1
@ MackleChan, saya membaca bahwa paradigma di sini adalah untuk memasukkan info ke dalam pesan yang mencoba untuk mengatakan 'apa yang terjadi, secara tata bahasa benar, dan kepura-puraan AI: ".. melalui konstruktor kosong berhasil, tapi ..." Betulkah? Coder forensik bagian dalam saya melihat ini sebagai konsekuensi evolusi dari kesalahan umum stack-trace-lost re-throws dan un disadari Exception.Data. Penekanannya harus menangkap telemetri. Refactoring di sini baik-baik saja, tetapi merindukan masalahnya.
radarbob

Jawaban:


10

Mengapa tidak memiliki kelas pengecualian khusus?

if (interfaceInstance == null)
{
    throw new ThisParticularEmptyConstructorException(<maybe a couple parameters>);
}

Itu mendorong pemformatan dan detail ke pengecualian itu sendiri, dan membuat kelas utama tidak berantakan.


1
Kelebihan: Sangat bersih dan teratur, memberikan nama yang bermakna untuk setiap pengecualian. Cons: berpotensi banyak kelas pengecualian tambahan .
FriendlyGuy

Jika itu penting, Anda bisa memiliki sejumlah kelas pengecualian, lulus kelas di mana pengecualian berasal sebagai parameter dan memiliki saklar raksasa di dalam kelas pengecualian yang lebih umum. Tapi baunya samar-samar - tidak yakin saya akan ke sana.
ptyx

baunya benar-benar buruk (perkecualian sangat erat dengan kelas-kelas). Saya kira akan sulit untuk menyeimbangkan pesan pengecualian yang dapat disesuaikan dengan jumlah pengecualian.
FriendlyGuy

7

Microsoft tampaknya (melalui melihat sumber .NET) terkadang menggunakan sumber daya / string Lingkungan. Sebagai contoh, ParseDecimal:

throw new OverflowException(Environment.GetResourceString("Overflow_Decimal"));

Pro:

  • Memusatkan pesan pengecualian, memungkinkan untuk digunakan kembali
  • Menjaga pesan pengecualian (bisa dibilang tidak penting untuk dikodekan) dari logika metode
  • Jenis pengecualian yang dilemparkan jelas
  • Pesan dapat dilokalisasi

Cons:

  • Jika satu pesan pengecualian diubah, semuanya berubah
  • Pesan pengecualian tidak mudah tersedia untuk kode yang melempar pengecualian.
  • Pesan statis dan tidak berisi informasi tentang nilai apa yang salah. Jika Anda ingin memformatnya, itu lebih berantakan dalam kode.

2
Anda mengabaikan manfaat yang sangat besar: Pelokalan teks pengecualian
17 dari 26

@ 17of26 - Poin bagus, menambahkannya.
FriendlyGuy

Saya mengubah jawaban Anda, tetapi pesan kesalahan yang dibuat dengan cara ini "statis"; Anda tidak dapat menambahkan pengubah kepada mereka seperti OP lakukan dalam kodenya. Jadi Anda telah secara efektif meninggalkan beberapa fungsinya.
Robert Harvey

@RobertHarvey - Saya menambahkannya sebagai penipu. Itu menjelaskan mengapa pengecualian bawaan tidak pernah benar-benar memberi Anda informasi lokal. Juga, FYI saya OP (saya tahu solusi ini, tetapi saya ingin tahu apakah orang lain memiliki solusi yang lebih baik).
FriendlyGuy

6
@ 17of26 sebagai pengembang, saya benci pengecualian lokal dengan hasrat. Saya harus menghapuskannya setiap kali sebelum saya bisa misalnya. google untuk solusi.
Konrad Morawski

2

Untuk skenario SDK publik, saya akan sangat mempertimbangkan menggunakan Kontrak Kode Microsoft karena ini memberikan kesalahan informatif, pemeriksaan statis dan Anda juga dapat menghasilkan dokumentasi untuk ditambahkan ke dalam dokumen XML dan file bantuan yang dihasilkan Sandcastle . Ini didukung di semua versi Visual Studio berbayar.

Keuntungan tambahan adalah bahwa jika pelanggan Anda menggunakan C #, mereka dapat memanfaatkan majelis referensi kontrak kode Anda untuk mendeteksi potensi masalah bahkan sebelum mereka menjalankan kode mereka.

Dokumentasi lengkap untuk Kontrak Kode ada di sini .


2

Teknik yang saya gunakan adalah menggabungkan, dan mengalihdayakan validasi dan membuang semuanya ke fungsi utilitas.

Satu-satunya manfaat terpenting adalah dikurangi menjadi satu-baris dalam logika bisnis .

Saya yakin Anda tidak dapat melakukan lebih baik kecuali Anda bisa menguranginya lebih lanjut - untuk menghilangkan semua validasi argumen dan penjaga keadaan-objek dari logika bisnis, hanya menjaga kondisi operasional yang luar biasa.

Ada, tentu saja, cara untuk melakukan itu - Bahasa yang diketik dengan kuat, desain "tidak ada objek tidak valid yang diizinkan kapan saja", Desain oleh Kontrak , dll.

Contoh:

internal static class ValidationUtil
{
    internal static void ThrowIfRectNullOrInvalid(int imageWidth, int imageHeight, Rect rect)
    {
        if (rect == null)
        {
            throw new ArgumentNullException("rect");
        }
        if (rect.Right > imageWidth || rect.Bottom > imageHeight || MoonPhase.Now == MoonPhase.Invisible)
        {
            throw new ArgumentException(
                message: "This is uselessly informative",
                paramName: "rect");
        }
    }
}

public class Thing
{
    public void DoSomething(Rect rect)
    {
        ValidationUtil.ThrowIfRectNullOrInvalid(_imageWidth, _imageHeight, rect);
        // rest of your code
    }
}

1

[note] Saya menyalin ini dari pertanyaan menjadi jawaban seandainya ada komentar tentang itu.

Pindahkan setiap pengecualian ke metode kelas, dengan mengambil argumen apa pun yang perlu diformat.

private Exception EmptyConstructor()
{
    string errMsg = string.Format(
          "Construction of Action Argument: {0}, via the empty constructor worked, but type: {1} could not be cast to type {2}.",
          ParameterInfo.Name,
          ParameterInfo.ParameterType,
          typeof(IParameter)
    );

    return new InvalidOperationException(errMsg);
}

Lampirkan semua metode pengecualian ke dalam wilayah, dan tempatkan di akhir kelas.

Pro:

  • Menyimpan pesan dari logika inti metode ini
  • Mengizinkan untuk menambahkan informasi logika ke setiap pesan (Anda dapat menyampaikan argumen ke metode ini)

Cons:

  • Metode berantakan. Berpotensi Anda bisa memiliki banyak metode yang hanya mengembalikan pengecualian dan tidak benar-benar terkait dengan logika bisnis.
  • Tidak dapat menggunakan kembali pesan di kelas lain

Saya pikir kontra yang Anda kutip jauh melebihi keuntungannya.
neontapir

@neontapir kontra semua mudah ditangani. Metode bantu harus diberikan IntentionRevealingNames dan dikelompokkan menjadi beberapa #region #endregion(yang menyebabkan mereka disembunyikan dari IDE secara default), dan jika mereka dapat diterapkan di kelas yang berbeda, masukkan mereka ke dalam internal static ValidationUtilitykelas. Btw, jangan pernah mengeluh tentang nama pengidentifikasi panjang di depan programmer C #.
rwong

Saya akan mengeluh tentang daerah. Menurut saya, jika Anda merasa ingin menggunakan daerah, kelas mungkin memiliki terlalu banyak tanggung jawab.
neontapir

0

Jika Anda bisa lolos dengan kesalahan yang sedikit lebih umum, Anda bisa menulis fungsi pemeran umum statis publik untuk Anda, yang menyimpulkan tipe sumber:

public static I CastOrThrow<I,T>(T t, string source)
{
    if (t is I)
        return (I)t;

    string errMsg = string.Format(
          "Failed to complete {0}, because type: {1} could not be cast to type {2}.",
          source,
          typeof(T),
          typeof(I)
        );

    throw new InvalidOperationException(errMsg);
}


/// and then:

var interfaceInstance = SdkHelper.CastTo<IParameter>(passedObject, "Action constructor");

Ada variasi yang mungkin (pikirkan SdkHelper.RequireNotNull()), yang hanya memeriksa persyaratan pada input, dan melempar jika gagal, tetapi dalam contoh ini menggabungkan para pemain dengan menghasilkan hasilnya adalah selfdocumenting dan kompak.

Jika Anda menggunakan .net 4.5, ada beberapa cara agar kompiler memasukkan nama metode / file saat ini sebagai parameter metode (lihat CallerMemberAttibute ). Tetapi untuk SDK, Anda mungkin tidak bisa meminta pelanggan Anda untuk beralih ke 4.5.


Ini lebih tentang pengecualian melempar secara umum (dan mengelola informasi dalam pengecualian vs berapa banyak itu mengacaukan kode), bukan tentang contoh khusus ini casting.
FriendlyGuy

0

Apa yang ingin kita lakukan untuk kesalahan logika bisnis (bukan kesalahan argumen, dll.) Adalah memiliki satu enum yang mendefinisikan semua jenis kesalahan potensial:

/// <summary>
/// This enum is used to identify each business rule uniquely.
/// </summary>
public enum BusinessRuleId {

    /// <summary>
    /// Indicates that a valid body weight value of a patient is missing for dose calculation.
    /// </summary>
    [Display(Name = @"DoseCalculation_PatientBodyWeightMissing")]
    PatientBodyWeightMissingForDoseCalculation = 1,

    /// <summary>
    /// Indicates that a valid body height value of a patient is missing for dose calculation.
    /// </summary>
    [Display(Name = @"DoseCalculation_PatientBodyHeightMissing")]
    PatientBodyHeightMissingForDoseCalculation = 2,

    // ...
}

The [Display(Name = "...")]atribut mendefinisikan kunci dalam sumber daya file yang akan digunakan untuk menerjemahkan pesan kesalahan.

Selain itu, file ini dapat digunakan sebagai titik awal untuk menemukan semua kejadian di mana jenis kesalahan tertentu dihasilkan dalam kode Anda.

Memeriksa Aturan Bisnis dapat didelegasikan ke kelas Validator khusus yang menghasilkan daftar Aturan Bisnis yang dilanggar.

Kami kemudian menggunakan jenis Pengecualian khusus untuk mengangkut aturan yang dilanggar:

[Serializable]
public class BusinessLogicException : Exception {

    /// <summary>
    /// The Business Rule that was violated.
    /// </summary>
    public BusinessRuleId ViolatedBusinessRule { get; set; }

    /// <summary>
    /// Optional: additional parameters to be used to during generation of the error message.
    /// </summary>
    public string[] MessageParameters { get; set; }

    /// <summary>
    /// This exception indicates that a Business Rule has been violated. 
    /// </summary>
    public BusinessLogicException(BusinessRuleId violatedBusinessRule, params string[] messageParameters) {
        ViolatedBusinessRule = violatedBusinessRule;
        MessageParameters = messageParameters;
    }
}

Panggilan layanan backend dibungkus dengan kode penanganan kesalahan umum, yang menerjemahkan Aturan Bisnis yang dilanggar menjadi pesan kesalahan yang dapat dibaca pengguna:

public object TryExecuteServiceAction(Action a) {
    try {
        return a();
    }
    catch (BusinessLogicException bex) {
        _logger.Error(GenerateErrorMessage(bex));
    }
}

public string GenerateErrorMessage(BusinessLogicException bex) {
    var translatedError = bex.ViolatedBusinessRule.ToTranslatedString();
    if (bex.MessageParameters != null) {
        translatedError = string.Format(translatedError, bex.MessageParameters);
    }
    return translatedError;
}

Berikut ToTranslatedString()adalah metode ekstensi untuk enumitu dapat membaca kunci sumber daya dari [Display]atribut dan menggunakan ResourceManageruntuk menerjemahkan kunci-kunci ini. Nilai untuk kunci sumber daya masing-masing dapat berisi placeholder string.Format, yang cocok dengan yang disediakan MessageParameters. Contoh entri dalam file resx:

<data name="DoseCalculation_PatientBodyWeightMissing" xml:space="preserve">
    <value>The dose can not be calculated because the body weight observation for patient {0} is missing or not up to date.</value>
    <comment>{0} ... Patient name</comment>
</data>

Contoh penggunaan:

throw new BusinessLogicException(BusinessRuleId.PatientBodyWeightMissingForDoseCalculation, patient.Name);

Dengan pendekatan ini, Anda dapat memisahkan generasi pesan kesalahan dari generasi kesalahan, tanpa perlu memperkenalkan kelas pengecualian baru untuk setiap jenis kesalahan baru. Berguna jika berbagai muka harus menampilkan pesan yang berbeda, jika pesan yang ditampilkan harus bergantung pada bahasa pengguna dan / atau peran dll.


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.