유익한 예외와 깨끗한 코드의 균형을 맞추는 좋은 방법은 무엇입니까? 공개 SDK를 사용하면 예외가

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

그러나 이것은 코드의 흐름을 혼란스럽게하는 경향이 있는데, 코드가하는 일보다는 오류 메시지에 많은 초점을 두는 경향이 있기 때문입니다.

동료가 다음과 같은 예외를 발생시키는 일부 예외를 리팩토링하기 시작했습니다.

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에 대해 묻고 있지만 다른 언어로 관리하는 방법도 도움이됩니다.

[편집하다]

각 접근법의 장단점을 갖는 것도 좋을 것입니다.



답변

특수한 예외 클래스가없는 이유는 무엇입니까?

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

그러면 형식 자체와 세부 정보가 예외 자체로 전달되고 기본 클래스가 정돈됩니다.


답변

Microsoft는 (.NET 소스를 통해) 때때로 리소스 / 환경 문자열을 사용하는 것 같습니다. 예를 들면 다음과 ParseDecimal같습니다.

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

장점 :

  • 예외 메시지를 중앙 집중화하여 재사용 가능
  • 예외 메시지 (논리적으로 중요하지 않은)를 메소드의 로직으로부터 멀리 유지
  • 발생되는 예외 유형이 명확합니다.
  • 메시지를 현지화 할 수 있습니다

단점 :

  • 하나의 예외 메시지가 변경되면 모두 변경됩니다.
  • 예외 메시지는 예외를 발생시키는 코드에서 쉽게 사용할 수 없습니다.
  • 메시지는 정적이며 어떤 값이 잘못되었는지에 대한 정보가 없습니다. 형식을 지정하려면 코드에서 더 복잡합니다.

답변

공개 SDK 시나리오의 경우, 정보 오류, 정적 검사를 제공하고 XML 문서 및 Sandcastle 생성 도움말 파일 에 추가 할 문서를 생성 할 수 있으므로 Microsoft Code Contracts 를 사용하는 것이 좋습니다 . 모든 유료 버전의 Visual Studio에서 지원됩니다.

또 다른 이점은 고객이 C #을 사용하는 경우 코드 계약 참조 어셈블리를 활용하여 코드를 실행하기 전에 잠재적 인 문제를 감지 할 수 있다는 것입니다.

코드 계약에 대한 전체 문서는 여기에 있습니다 .


답변

내가 사용하는 기술 은 유효성 검사결합하고 아웃소싱하여 유틸리티 기능에 모두 아웃소싱하는 것입니다.

가장 중요한 이점은 비즈니스 로직에서 한 줄로 줄어든다 는 것입니다 .

비즈니스 논리에서 모든 인수 유효성 검사 및 객체 상태 가드를 제거하고 예외적 인 운영 조건 만 유지하면 더 줄일 수 없다면 더 잘할 수 없습니다.

물론 그렇게 할 수있는 방법이 있습니다. 강력하게 형식화 된 언어, “언제든지 유효하지 않은 객체는 허용되지 않습니다”디자인, 계약의한 디자인

예:

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

답변

[참고] 질문에 대한 의견이있는 경우 질문에서 답변으로 복사했습니다.

형식화가 필요한 인수를 사용하여 각 예외를 클래스의 메소드로 이동하십시오.

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

모든 예외 메소드 를 지역으로 묶고 클래스의 끝에 배치하십시오.

장점 :

  • 메소드의 핵심 로직에서 메시지를 유지합니다.
  • 각 메시지에 논리 정보를 추가 할 수 있습니다 (메서드에 인수를 전달할 수 있음)

단점 :

  • 방법 혼란. 잠재적으로 예외를 반환하고 실제로 비즈니스 로직과 관련이없는 많은 메소드를 가질 수 있습니다.
  • 다른 클래스에서 메시지를 재사용 할 수 없습니다

답변

좀 더 일반적인 오류로 벗어날 수 있다면 공개 정적 일반 캐스트 함수를 작성할 수 있으며 소스 유형을 유추합니다.

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로 전환하지 않아도됩니다.


답변

비즈니스 논리 오류 (필수적으로 인수 오류 등이 아님)에 대해 우리가하고 싶은 것은 모든 잠재적 유형의 오류를 정의하는 단일 열거 형을 갖는 것입니다.

/// <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 = "...")]속성은 자원의 키가 오류 메시지를 변환하는 데 사용하는 파일을 정의합니다.

또한이 파일을 코드에서 특정 유형의 오류가 생성되는 모든 발생을 찾기위한 시작점으로 사용할 수 있습니다.

비즈니스 규칙 확인은 위반 된 비즈니스 규칙 목록을 생성하는 특수 유효성 검사기 클래스에 위임 될 수 있습니다.

그런 다음 사용자 정의 예외 유형을 사용하여 위반 된 규칙을 전송합니다.

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

백엔드 서비스 호출은 일반 오류 처리 코드로 랩핑되어 위반 된 비즈니스 규칙을 사용자가 읽을 수있는 오류 메시지로 변환합니다.

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

이 방법을 사용하면 각각의 새로운 유형의 오류에 대해 새 예외 클래스를 도입 할 필요없이 오류 메시지 생성과 오류 생성을 분리 할 수 ​​있습니다. 표시되는 메시지가 사용자 언어 및 / 또는 역할 등에 의존해야하는 경우 서로 다른 프런트 엔드에 다른 메시지를 표시해야하는 경우에 유용합니다.