อะไรคือวิธีที่ดีในการปรับสมดุลข้อยกเว้นที่ให้ข้อมูลและรหัสที่สะอาด


11

ด้วย SDK สาธารณะของเราเรามักจะต้องการส่งข้อความที่ให้ข้อมูลอย่างมากเกี่ยวกับสาเหตุที่เกิดข้อยกเว้น ตัวอย่างเช่น:

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);
}

อย่างไรก็ตามสิ่งนี้มีแนวโน้มที่จะทำให้การไหลเวียนของรหัสแย่ลงเนื่องจากมีแนวโน้มที่จะให้ความสำคัญกับข้อความแสดงข้อผิดพลาดมากกว่าที่จะทำโค้ด

เพื่อนร่วมงานเริ่ม refactoring ข้อยกเว้นบางอย่างให้กับสิ่งนี้:

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);
}

ซึ่งทำให้เข้าใจรหัสได้ง่ายขึ้น แต่เพิ่มวิธีพิเศษมากมายในการจัดการข้อผิดพลาด

มีวิธีอื่นใดบ้างในการหลีกเลี่ยงปัญหา ฉันถามเกี่ยวกับสำนวน C # /. NET เป็นหลัก แต่วิธีการจัดการภาษาอื่น ๆ ก็มีประโยชน์เช่นกัน

[แก้ไข]

มันจะดีถ้ามีข้อดีข้อเสียของแต่ละวิธีเช่นกัน


4
IMHO วิธีแก้ปัญหาของเพื่อนร่วมงานของคุณนั้นดีมากและฉันเดาว่าถ้าคุณได้รับวิธีการพิเศษมากมายจริง ๆ คุณสามารถนำกลับมาใช้ใหม่ได้อย่างน้อยบางส่วน การมีวิธีการขนาดเล็กจำนวนมากนั้นใช้ได้เมื่อวิธีการของคุณได้รับการตั้งชื่ออย่างดีตราบใดที่พวกเขาสร้างหน่วยการสร้างที่เข้าใจง่ายของโปรแกรมของคุณ - นี่อาจเป็นกรณีที่นี่
Doc Brown

@DocBrown - ใช่ฉันชอบความคิด (เพิ่มเป็นคำตอบด้านล่างพร้อมกับข้อดี / ข้อเสีย) แต่สำหรับทั้ง intellisense และสำหรับจำนวนวิธีการที่อาจเกิดขึ้นก็เริ่มมีลักษณะรกรุงรังเช่นกัน
FriendlyGuy

1
ความคิด: อย่าส่งข้อความถึงผู้ให้บริการทุกรายละเอียดข้อยกเว้น การรวมกันของการใช้Exception.Dataคุณสมบัติข้อยกเว้น "จู้จี้จุกจิก" การเรียกใช้การจับโค้ดและเพิ่มบริบทของตัวเองพร้อมกับสแต็คการโทรที่ดักจับข้อมูลส่วนร่วมทั้งหมดที่ควรอนุญาตข้อความ verbose น้อยกว่ามาก ในที่สุดก็System.Reflection.MethodBaseดูมีแนวโน้มที่จะให้รายละเอียดเพื่อส่งผ่านไปยัง "การก่อสร้างข้อยกเว้น" ของคุณ
Radarbob

@radarbob คุณกำลังบอกว่าอาจจะมีข้อยกเว้นที่ละเอียดเกินไป? บางทีเรากำลังทำมันมากเกินไปเช่นการเข้าสู่ระบบ
FriendlyGuy

1
@ กุญแจมือฉันอ่านว่ากระบวนทัศน์ที่นี่คือการใส่ข้อมูลลงในข้อความที่พยายามจะบอกว่า 'เกิดอะไรขึ้นจำเป็นต้องถูกต้องตามหลักไวยากรณ์และข้ออ้างของ AI: ".. ผ่านตัวสร้างเปล่าที่ทำงาน แต่ .. " จริงๆ? โคเดอร์นิติวิทยาศาสตร์ด้านในของฉันมองว่านี่เป็นผลสืบเนื่องจากวิวัฒนาการของข้อผิดพลาดทั่วไปของการทิ้งสแต็ก - รอย - แพ้และการไม่รู้ตัวException.Dataอีกครั้ง ควรเน้นจับ telemetry การรีแฟคเตอร์ที่นี่เป็นเรื่องปกติ แต่มันก็พลาดปัญหา
Radarbob

คำตอบ:


10

ทำไมไม่มีคลาสยกเว้นพิเศษ?

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

ซึ่งจะผลักดันการจัดรูปแบบและรายละเอียดไปยังข้อยกเว้นและทำให้คลาสหลักไม่มีการกระจาย


1
จุดเด่น: สะอาดและจัดระเบียบมากให้ชื่อที่มีความหมายสำหรับแต่ละข้อยกเว้น ข้อด้อย: อาจมีคลาสข้อยกเว้นพิเศษจำนวนมาก
FriendlyGuy

หากมีความสำคัญคุณอาจมีคลาสยกเว้นจำนวน จำกัด ผ่านคลาสที่มีข้อยกเว้นเกิดขึ้นเป็นพารามิเตอร์และมีสวิตช์ขนาดยักษ์ภายในคลาสยกเว้นพิเศษเพิ่มเติม แต่นั่นมีกลิ่นจาง ๆ ไป - ไม่แน่ใจว่าฉันจะไปที่นั่น
ptyx

มันมีกลิ่นไม่ดีจริง ๆ (ยกเว้นคู่อย่างแน่นหนากับคลาส) ฉันเดาว่าคงเป็นการยากที่จะปรับสมดุลข้อความยกเว้นที่ปรับแต่งได้เทียบกับจำนวนของข้อยกเว้น
FriendlyGuy

7

Microsoft ดูเหมือนว่า (โดยดูจากแหล่ง. NET) บางครั้งใช้สตริงทรัพยากร / สภาพแวดล้อม ตัวอย่างเช่นParseDecimal:

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

ข้อดี:

  • การรวมข้อความข้อยกเว้นไว้ที่ส่วนกลางทำให้สามารถใช้ซ้ำได้
  • การเก็บข้อความข้อยกเว้น (ซึ่งไม่สำคัญกับรหัส) อยู่ห่างจากตรรกะของวิธีการ
  • ประเภทของข้อยกเว้นที่ถูกโยนมีความชัดเจน
  • ข้อความสามารถแปลเป็

จุดด้อย:

  • หากข้อความข้อยกเว้นมีการเปลี่ยนแปลงพวกเขาทั้งหมดเปลี่ยน
  • ข้อความแสดงข้อยกเว้นนั้นไม่สามารถใช้งานได้ง่ายสำหรับรหัสที่ทำให้เกิดข้อยกเว้น
  • ข้อความเป็นแบบสแตติกและไม่มีข้อมูลเกี่ยวกับค่าใดที่ไม่ถูกต้อง หากคุณต้องการจัดรูปแบบมันเป็นความยุ่งเหยิงในรหัส

2
คุณได้รับประโยชน์มากมาย: การแปลข้อความยกเว้นเป็นภาษาท้องถิ่น
17 จาก 26

@ 17of26 - จุดดีเพิ่มเข้ามา
FriendlyGuy

ฉันยกระดับคำตอบของคุณ แต่ข้อความแสดงข้อผิดพลาดที่ทำในลักษณะนี้คือ "คงที่"; คุณไม่สามารถเพิ่มตัวดัดแปลงให้กับพวกเขาเช่น OP กำลังทำในรหัสของเขา ดังนั้นคุณจึงเหลือฟังก์ชันการทำงานบางส่วนของเขา
Robert Harvey

@RobertHarvey - ฉันเพิ่มมันเป็นข้อผิดพลาด สิ่งนี้อธิบายได้ว่าทำไมข้อยกเว้นในตัวไม่ให้ข้อมูลท้องถิ่นแก่คุณ นอกจากนี้ FYI ฉันเป็น OP (ฉันรู้วิธีนี้ แต่ฉันอยากรู้ว่าคนอื่นมีวิธีแก้ปัญหาที่ดีกว่านี้)
FriendlyGuy

6
@ 17of26 ในฐานะนักพัฒนาฉันเกลียดการยกเว้นภาษาท้องถิ่นด้วยความหลงใหล ฉันต้องยกเลิกการแปลพวกเขาทุกครั้งก่อนที่ฉันจะทำได้ google สำหรับโซลูชั่น
Konrad Morawski

2

สำหรับสถานการณ์ SDK สาธารณะฉันจะพิจารณาใช้รหัสสัญญาของ Microsoftอย่างยิ่งเนื่องจากสิ่งเหล่านี้จะให้ข้อผิดพลาดในข้อมูลการตรวจสอบแบบคงที่และคุณยังสามารถสร้างเอกสารประกอบเพื่อเพิ่มลงในเอกสาร XML และไฟล์ช่วยเหลือที่สร้างโดยSandcastle ได้รับการสนับสนุนในรุ่น Visual Studio ที่ชำระเงินทั้งหมด

ข้อได้เปรียบเพิ่มเติมคือถ้าลูกค้าของคุณใช้ C # พวกเขาสามารถใช้ประโยชน์จากแอสเซมบลีการอ้างอิงสัญญารหัสของคุณเพื่อตรวจสอบปัญหาที่อาจเกิดขึ้นก่อนที่พวกเขาจะเรียกใช้รหัสของพวกเขา

เอกสารฉบับเต็มสำหรับสัญญารหัสที่นี่


2

เทคนิคที่ฉันใช้คือการรวมและการตรวจสอบความถูกต้องภายนอกและส่งออกไปยังฟังก์ชันอรรถประโยชน์

เดียวประโยชน์ที่สำคัญที่สุดก็คือว่ามันจะลดลงไปหนึ่งซับในตรรกะทางธุรกิจ

ฉันพนันได้เลยว่าคุณจะไม่สามารถทำได้ดีกว่านี้หากคุณไม่สามารถลดได้อีก - เพื่อกำจัดการตรวจสอบข้อโต้แย้งทั้งหมดและผู้ปกป้องวัตถุจากตรรกะทางธุรกิจโดยรักษาเฉพาะเงื่อนไขการปฏิบัติงานที่ยอดเยี่ยม

แน่นอนว่ามีวิธีในการทำเช่นนั้น - ภาษาที่พิมพ์อย่างรุนแรงการออกแบบ "ไม่อนุญาตวัตถุที่ไม่ถูกต้องทุกเวลา" การออกแบบตามสัญญาฯลฯ

ตัวอย่าง:

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

[หมายเหตุ] ฉันคัดลอกสิ่งนี้จากคำถามไปเป็นคำตอบในกรณีที่มีความคิดเห็นเกี่ยวกับมัน

ย้ายข้อยกเว้นแต่ละข้อไปยังวิธีการเรียนโดยใช้อาร์กิวเมนต์ใด ๆ ที่ต้องการการจัดรูปแบบ

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);
}

ใส่เมธอดข้อยกเว้นทั้งหมดลงในพื้นที่และวางไว้ที่ส่วนท้ายของคลาส

ข้อดี:

  • เก็บข้อความจากตรรกะหลักของวิธีการ
  • ช่วยให้การเพิ่มข้อมูลตรรกะในแต่ละข้อความ (คุณสามารถส่งผ่านข้อโต้แย้งไปยังวิธีการ)

จุดด้อย:

  • วิธีการถ่วง อาจเป็นไปได้ว่าคุณมีวิธีการมากมายที่จะคืนค่าข้อยกเว้นและไม่เกี่ยวข้องกับตรรกะทางธุรกิจจริงๆ
  • ไม่สามารถใช้ข้อความซ้ำในคลาสอื่นได้

ฉันคิดว่าข้อเสียที่คุณอ้างไปนั้นมีค่ามากกว่าข้อดี
neontapir

@neontapir ข้อเสียทั้งหมดได้อย่างง่ายดายแก้ไข ควรได้รับวิธีการช่วยเสริม IntentionRevealingNames และจัดกลุ่มเป็นบางส่วน#region #endregion(ซึ่งทำให้พวกเขาถูกซ่อนจาก IDE โดยค่าเริ่มต้น) และถ้าพวกเขาจะใช้ได้ในชั้นเรียนที่แตกต่างกันวางไว้ในinternal static ValidationUtilityชั้นเรียน แต่อย่าบ่นเกี่ยวกับชื่อตัวระบุที่ยาวต่อหน้าโปรแกรมเมอร์ C #

ฉันจะบ่นเกี่ยวกับภูมิภาคท ในใจของฉันหากคุณพบว่าตัวเองต้องการหันไปทางภูมิภาคชั้นเรียนอาจมีความรับผิดชอบมากเกินไป
neontapir

0

หากคุณสามารถแก้ไขข้อผิดพลาดทั่วไปได้อีกเล็กน้อยคุณสามารถเขียนฟังก์ชั่นคาสติ้งทั่วไปแบบคงที่ให้กับคุณซึ่งจะทำให้ประเภทแหล่งที่มา:

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");

มีรูปแบบที่เป็นไปได้ (คิดSdkHelper.RequireNotNull()) ว่าจะตรวจสอบเพียงข้อกำหนดเกี่ยวกับอินพุตและโยนหากมันล้มเหลว แต่ในตัวอย่างนี้การรวมตัวบล็อกกับการสร้างผลลัพธ์คือการทำเอกสารตัวเองและกะทัดรัด

หากคุณอยู่ใน. net 4.5 มีวิธีให้คอมไพเลอร์แทรกชื่อของวิธีการ / ไฟล์ปัจจุบันเป็นพารามิเตอร์วิธี (ดูที่CallerMemberAttibute ) แต่สำหรับ SDK คุณอาจไม่ต้องการให้ลูกค้าเปลี่ยนเป็น 4.5


มันเกี่ยวกับข้อยกเว้นที่เกิดขึ้นโดยทั่วไป (และการจัดการข้อมูลในข้อยกเว้นเทียบกับจำนวนที่ จำกัด รหัส) ไม่เกี่ยวกับตัวอย่างที่เฉพาะเจาะจงในการคัดเลือกนักแสดง
FriendlyGuy

0

สิ่งที่เราต้องการทำเพื่อข้อผิดพลาดทางตรรกะทางธุรกิจ (ไม่ใช่ข้อผิดพลาดการโต้แย้งที่ไม่จำเป็น ฯลฯ ) คือการมี enum เพียงตัวเดียวที่กำหนดข้อผิดพลาดประเภทที่อาจเกิดขึ้นทั้งหมด:

/// <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,

    // ...
}

แอ[Display(Name = "...")]ททริบิวกำหนดคีย์ในไฟล์ทรัพยากรที่จะใช้ในการแปลข้อความผิดพลาด

นอกจากนี้ไฟล์นี้ยังสามารถใช้เป็นจุดเริ่มต้นเพื่อค้นหาเหตุการณ์ทั้งหมดที่เกิดข้อผิดพลาดบางประเภทในรหัสของคุณ

การตรวจสอบกฎธุรกิจสามารถมอบหมายให้คลาส Validator เฉพาะที่ให้รายชื่อกฎธุรกิจที่ถูกละเมิด

จากนั้นเราจะใช้ประเภทข้อยกเว้นที่กำหนดเองเพื่อส่งผ่านกฎที่ละเมิด:

[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;
    }
}

การเรียกใช้บริการแบ็คเอนด์ถูกห่อด้วยรหัสการจัดการข้อผิดพลาดทั่วไปซึ่งแปลกฎ Busines ที่ละเมิดไปเป็นข้อความแสดงข้อผิดพลาดที่ผู้ใช้สามารถอ่านได้:

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;
}

นี่ToTranslatedString()คือวิธีส่วนขยายสำหรับenumที่สามารถอ่านคีย์ทรัพยากรจาก[Display]แอตทริบิวต์และใช้ResourceManagerเพื่อแปลคีย์เหล่านี้ ค่าสำหรับคีย์ทรัพยากรที่เกี่ยวข้องสามารถมีตัวยึดสำหรับstring.Formatซึ่งตรงกับที่ให้MessageParametersไว้ ตัวอย่างของรายการในไฟล์ 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>

ตัวอย่างการใช้งาน:

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

ด้วยวิธีนี้คุณสามารถแยกการสร้างข้อความแสดงข้อผิดพลาดจากการสร้างข้อผิดพลาดโดยไม่จำเป็นต้องแนะนำคลาสยกเว้นใหม่สำหรับข้อผิดพลาดแต่ละประเภทใหม่ มีประโยชน์หากส่วนหน้าที่แตกต่างกันควรแสดงข้อความที่แตกต่างกันหากข้อความที่แสดงควรขึ้นอยู่กับภาษาของผู้ใช้และ / หรือบทบาทเป็นต้น


0

ฉันจะใช้คำสั่งป้องกันในตัวอย่างนี้เพื่อตรวจสอบพารามิเตอร์สามารถนำมาใช้ใหม่

นอกจากนี้คุณสามารถใช้รูปแบบการสร้างเพื่อตรวจสอบมากกว่าหนึ่งสามารถถูกผูกมัด ก็จะมีลักษณะคล้าย ๆยืนยันได้อย่างคล่องแคล่ว

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.