유익한 예외와 깨끗한 코드의 균형을 맞추는 좋은 방법은 무엇입니까?


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

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

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

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-그래, 나는 아이디어 (장단점과 함께 아래 답변으로 추가됨)를 좋아하지만, 지능과 잠재적 인 방법의 수 모두에 대해 혼란스러워 보이기 시작합니다.
FriendlyGuy

1
생각 : 메시지 를 모든 예외 세부 정보 전달자로 만들지 마십시오 . Exception.Data캡처 된 호출 스택과 함께 "픽키 (picky)"예외 잡기, 호출 코드 잡기 및 자체 컨텍스트 추가 속성 의 조합을 사용하면 정보를보다 상세하게 전달할 수 있습니다. 마지막으로 System.Reflection.MethodBase"예외 구성"방법으로 전달할 세부 정보를 제공 할 것으로 기대됩니다.
radarbob

@ radarbob 당신은 아마도 예외가 너무 장황하다고 제안하고 있습니까? 아마도 우리는 로깅과 너무 비슷하게 만들고 있습니다.
FriendlyGuy

1
@MackleChan, 여기서 패러다임은 정보를 메시지에 넣는 것입니다. '정확하게 무슨 일이 있었는지, 문법적으로 정확하며 AI의 척도입니다. 정말? 내 내부 법의학 코더는 이것을 스택 추적 손실 다시 던지기의 일반적인 오류와에 대한 인식의 진화 결과로 본다 Exception.Data. 원격 측정 캡처가 강조되어야합니다. 여기에서 리팩토링하는 것은 좋지만 문제가 없습니다.
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 of 26

@ 17of26-좋은 지적, 추가했습니다.
FriendlyGuy

귀하의 답변을 올바로 보냈지 만 이런 방식으로 작성된 오류 메시지는 "정적"입니다. OP가 코드에서 수행하는 것처럼 수정자를 추가 할 수 없습니다. 그래서 당신은 그의 기능 중 일부를 효과적으로 배제했습니다.
Robert Harvey

@RobertHarvey-나는 죄수로 추가했습니다. 내장 예외가 실제로 로컬 정보를 제공하지 않는 이유를 설명합니다. 또한 참고로 OP입니다 (이 솔루션에 대해 알고 있었지만 다른 사람들이 더 나은 솔루션을 가지고 있는지 알고 싶었습니다).
FriendlyGuy

6
@ 17of26 개발자로서 열정으로 현지화 된 예외를 싫어합니다. 예를 들어 매번 지역화를 해제해야합니다. 솔루션을위한 Google.
Konrad Morawski

2

공개 SDK 시나리오의 경우, 정보 오류, 정적 검사를 제공하고 XML 문서 및 Sandcastle 생성 도움말 파일 에 추가 할 문서를 생성 할 수 있으므로 Microsoft Code Contracts 를 사용하는 것이 좋습니다 . 모든 유료 버전의 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. Btw, C # 프로그래머 앞에서 긴 식별자 이름에 대해 불평하지 마십시오.
rwong

그래도 지역에 대해 불평합니다. 내 생각에, 만약 당신이 당신이 지역에 의지하기를 원한다면, 학급은 아마도 너무 많은 책임을 가졌을 것입니다.
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

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

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

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


0

이 예 에서와 같이 가드 절을 사용 하여 매개 변수 확인을 재사용 할 수 있습니다.

또한 빌더 패턴을 사용하여 둘 이상의 유효성 검증을 연결할 수 있습니다. 유창한 주장 처럼 보일 것 입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.