전체 모듈 사용 또는 공개 메소드의 인수 만 검증해야합니까?


9

공개 메소드의 인수를 검증하는 것이 권장된다고 들었습니다.

동기 부여는 이해할 수 있습니다. 모듈이 잘못된 방식으로 사용될 경우 예측할 수없는 동작 대신 즉시 예외를 처리하려고합니다.

나를 괴롭히는 것은 잘못된 인수가 모듈을 사용하는 동안 발생할 수있는 유일한 오류가 아니라는 것입니다. 권장 사항을 따르고 오류 에스컬레이션을 원하지 않는 경우 검사 로직을 추가해야하는 오류 시나리오는 다음과 같습니다.

  • 수신 전화-예기치 않은 인수
  • 수신 전화-모듈이 잘못된 상태입니다
  • 외부 전화-예기치 않은 결과가 반환되었습니다.
  • 외부 통화-예상치 못한 부작용 (호출 모듈에 이중 입력, 다른 종속성 상태 중단)

나는이 모든 조건을 고려하고 하나의 방법 (죄송하지만 C #이 아닌 사람들)으로 간단한 모듈을 작성하려고했습니다.

public sealed class Room
{
    private readonly IDoorFactory _doorFactory;
    private bool _entered;
    private IDoor _door;
    public Room(IDoorFactory doorFactory)
    {
        if (doorFactory == null)
            throw new ArgumentNullException("doorFactory");
        _doorFactory = doorFactory;
    }
    public void Open()
    {
        if (_door != null)
            throw new InvalidOperationException("Room is already opened");
        if (_entered)
            throw new InvalidOperationException("Double entry is not allowed");
        _entered = true;
        _door = _doorFactory.Create();
        if (_door == null)
            throw new IncompatibleDependencyException("doorFactory");
        _door.Open();
        _entered = false;
    }
}

이제 안전합니다 =)

꽤 오싹합니다. 그러나 수십 개의 메소드, 복잡한 상태 및 많은 외부 호출 (hi, 의존성 주입 애호가)이있는 실제 모듈에 얼마나 소름 끼칠지 상상해보십시오. 동작을 무시할 수있는 모듈 (C #에서 봉인되지 않은 클래스)을 호출하는 경우 외부 호출을 수행하므로 호출자의 범위에서 결과를 예측할 수 없습니다.

요약하면, 올바른 방법은 무엇이며 왜? 아래 옵션 중에서 선택할 수 있으면 추가 질문에 대답하십시오.

전체 모듈 사용량을 확인하십시오. 단위 테스트가 필요합니까? 그러한 코드의 예가 있습니까? 의존성 주입은 사용량이 제한되어야합니까 (더 많은 검사 논리가 발생하기 때문에)? 해당 검사를 디버그 시간으로 옮기는 것이 실용적이지 않습니까 (릴리스에 포함하지 않음)?

인수 만 확인하십시오. 내 경험상 인수 검사, 특히 null 검사는 가장 효과적인 검사입니다. 인수 오류는 복잡한 실수와 오류 에스컬레이션을 거의 일으키지 않기 때문입니다. 대부분의 경우 NullReferenceException다음 줄에 표시됩니다. 그렇다면 인수 확인이 왜 그렇게 특별합니까?

모듈 사용량을 확인하지 마십시오. 꽤 인기가없는 의견입니다. 이유를 설명해 주시겠습니까?


변이가 유지되도록 필드 할당 중에 점검해야합니다.
Basilevs

@Basilevs Interesting ... Code Contracts 이데올로기 나 오래된 것입니까? 읽을만한 것을 추천 할 수 있습니까 (댓글과 관련이 있습니까)?
astef

우려의 기본 분리입니다. 코드 중복이 최소화되고 책임이 잘 정의되어 있지만 모든 사례가 다루어집니다.
Basilevs

@Basilevs 따라서 다른 모듈의 동작을 확인하지 말고 자체 상태 불변성을 확인하십시오. 합리적으로 들립니다. 그러나 인수 확인에 대한 관련 질문 에서이 간단한 영수증이 보이지 않는 이유는 무엇입니까?
astef

글쎄, 일부 점검은 여전히 ​​필요하지만, 다른 곳으로 전달 된 값이 아니라 실제로 사용 된 값에 대해서만 수행되어야합니다. 예를 들어 클라이언트 코드에서 인덱스를 확인하는 대신 List 구현을 사용하여 OOB 오류를 확인합니다. 일반적으로 낮은 수준의 프레임 워크 오류이며 수동으로 생성 할 필요가 없습니다.
Basilevs

답변:


2

TL; DR : 현재 상태의 [유효성]에 의존하여 상태 변경을 검증합니다.

아래에서는 릴리스 가능 확인 만 고려합니다. 디버그 전용 활성 어설 션은 문서 형식으로, 고유 한 방식으로 유용하며이 질문의 범위를 벗어납니다.

다음 원리를 고려하십시오.

  • 상식
  • 빨리 실패
  • 마른
  • SRP

정의

  • 구성 요소-API를 제공하는 단위
  • 클라이언트-컴포넌트 API 사용자

가변 상태

문제

명령형 언어에서 오류 증상과 그 원인은 몇 시간 동안 많이 들려서 분리 될 수 있습니다. 현재 상태를 검사하면 전체적인 손상 프로세스를 파악할 수 없으므로 오류의 원인을 알 수 없으므로 상태 손상 자체가 숨겨져 서 변경 불가능한 결과를 초래할 수 있습니다.

해결책

국가의 모든 변화는 신중하게 제작되고 검증되어야합니다. 변경 가능한 상태를 처리하는 한 가지 방법은 상태를 최소로 유지하는 것입니다. 이것은 다음에 의해 달성됩니다.

  • 형식 시스템 (const 및 final member 선언)
  • 불변량 소개
  • 공개 API를 통해 구성 요소 상태의 모든 변경 사항 확인

컴포넌트의 상태를 확장 할 때 컴파일러가 새로운 데이터의 불변성을 강제하도록함으로써 그렇게하는 것을 고려하십시오. 또한 모든 현명한 런타임 제약 조건을 적용하여 잠재적 결과 상태를 가능한 한 잘 정의 된 가장 작은 집합으로 제한합니다.

// Wrong
class Natural {
    private int number;
    public Natural(int number) {
        this.number = number;
    }
    public int getInt() {
      if (number < 1)
          throw new InvalidOperationException();
      return number;
    }
}

// Right
class Natural {
    private readonly int number;
    /**
     * @param number - positive number
     */
    public Natural(int number) {
      // Going to modify state, verification is required
      if (number < 1)
        throw new ArgumentException("Natural number should be  positive: " + number);
      this.number = number;
    }
    public int getInt() {
      // State is guaranteed by construction and compiler
      return number;
    }
}

반복과 책임 응집

문제

작업의 전제 조건과 사후 조건을 확인하면 클라이언트와 구성 요소 모두에서 확인 코드가 복제됩니다. 구성 요소 호출의 유효성 검사는 종종 클라이언트가 구성 요소의 책임 중 일부를 수행하도록합니다.

해결책

가능한 경우 구성 요소를 사용하여 상태 확인을 수행하십시오. 구성 요소는 구성 요소 상태를 명확하게 정의하기 위해 특수한 사용 검증 (예 : 인수 검증 또는 조작 순서 시행)이 필요하지 않은 API를 제공해야합니다. 필요한 경우 API 호출 인수를 확인하고 필요한 수단으로 실패를보고하며 상태 손상을 방지하기 위해 노력합니다.

클라이언트는 구성 요소를 사용하여 API 사용을 확인해야합니다. 반복을 피할뿐만 아니라 클라이언트는 더 이상 구성 요소의 추가 구현 정보에 의존하지 않습니다. 프레임 워크를 구성 요소로 고려하십시오. 컴포넌트의 불변 값이 충분히 엄격하지 않거나 컴포넌트 예외를 구현 세부 사항으로 캡슐화 할 때만 사용자 정의 확인 코드를 작성하십시오.

조작이 상태를 변경하지 않고 상태 변경 검증에 포함되지 않는 경우 가능한 모든 레벨에서 모든 인수를 확인하십시오.

class Store {
  private readonly List<int> slots = new List<int>();
  public void putToSlot(int slot, int data) {
    if (slot < 0 || slot >= slots.Count) // Unnecessary, validated by List, only needed for custom error message
      throw new ArgumentException("data");
    slots[slot] = data;
  }
}

class Natural {
   int _number;
   public Natural(int number) {
       if (number < 1)
          number = 1;  //Wrong: client can't rely on argument verification, additional state uncertainity is introduced.  Right: throw new ArgumentException(number);
       _number = number;
   }
}

대답

설명 된 원칙이 해당 예에 적용되면 다음과 같은 이점이 있습니다.

public sealed class Room
{
    private bool _entered = false;
    // Do not use lazy instantiation if not absolutely necessary, this introduces additional mutable state
    private readonly IDoor _door;
    public Room(IDoorFactory doorFactory)
    {
        // Rely on system null check
        IDoor door = _doorFactory.Create();
        // Modifying own state, verification is required
        if (door == null)
           throw new ArgumentNullException("Door");
        _door = door;
    }
    public void Enter()
    {
        // Room invariants do not guarantee _entered value. Door state is indirectly a part of our state. Verification is required to prevent second door state change below.
        if (_entered)
           throw new InvalidOperationException("Double entry is not allowed");
        _entered = true;     
        // rely on immutability for _door field to be non-null
        // rely on door implementation to control resulting door state       
        _door.Open();            
    }
}

요약

고객의 상태는 자체 필드 값과 구성 요소 상태의 일부로 구성되며 자체 불변 값으로 처리되지 않습니다. 확인은 클라이언트의 실제 상태 변경 이전에 수행해야합니다.


1

클래스는 자체 상태를 담당합니다. 따라서 물건을 수용 가능한 상태로 유지하거나 넣는 정도까지 확인하십시오.

모듈이 잘못된 방식으로 사용될 경우 예측할 수없는 동작 대신 즉시 예외를 처리하려고합니다.

아니요, 예외를 던지지 말고 예측 가능한 행동을 전달하십시오. 주정부의 책임은 클래스 / 응용 프로그램을 실용적으로 허용하는 것입니다. 예를 들어, 전달 nullaCollection.Add()? 추가하고 계속하지 마십시오. 당신은 얻을 null객체를 생성하기위한 입력을? null 객체 또는 기본 객체를 만듭니다. 위의 door이미 open? 그래서 계속하세요. DoorFactory인수가 null입니까? 새로운 것을 만드십시오. 내가 만들 때 enum항상 Undefined멤버가 있습니다. 나는 Dictionarys를 자유롭게 사용 enums하고 사물을 명시 적으로 정의합니다. 이것은 예측 가능한 행동을 제공하는 데 먼 길을갑니다

(안녕하세요, 의존성 주입 애호가!)

그러나 나는 매개 변수 골짜기의 그림자를 뚫고 있지만 논쟁을 두려워하지 않을 것입니다. 앞에서도 가능한 한 기본 및 선택적 매개 변수를 사용합니다.

위의 모든 것은 내부 처리가 계속 진행되도록합니다. 특정 응용 프로그램에는 예외가 발생하는 단일 장소만으로 여러 클래스에 수십 개의 메소드가 있습니다. 그럼에도 불구하고, 그것은 널 인수 때문이 아니거나 처리를 계속할 수 없다는 것이 코드가 "비 기능적"/ "널"오브젝트를 작성했기 때문입니다.

편집하다

내 의견을 인용하면 전체입니다. 나는 'null'을 만났을 때 디자인이 단순히 "포기"해서는 안된다고 생각합니다. 특히 복합 객체를 사용합니다.

여기서 핵심 개념 / 가정을 잊어 버리고 있습니다- encapsulation& single responsibility. 첫 번째 클라이언트 상호 작용 계층 이후에는 사실상 널 검사가 없습니다. 이 코드는 허용 강력한. 클래스는 기본 상태로 설계되었으므로 상호 작용하는 코드가 버그를 타거나 악의적 인 정크처럼 작성되지 않고 작동합니다. 컴포지트 부모는 유효성을 평가하기 위해 하위 계층에 도달 할 필요가 없습니다 (의미하여 모든 구석과 채석장에서 null을 확인). 부모는 자녀의 기본 상태가 무엇을 의미하는지 알고 있습니다

편집 종료


1
잘못된 컬렉션 요소를 추가하지 않으면 매우 예측할 수없는 동작입니다.
Basilevs

1
모든 인터페이스가 일상적인 오류로 인해 관용적 인 방식으로 설계된다면, 프로그램은 우연히 깨어나 인류를 파괴 할 것입니다.
astef

여기서 핵심 개념 / 가정을 잊어 버리고 있습니다- encapsulation& single responsibility. null첫 번째 클라이언트 상호 작용 계층 이후에는 거의 점검 이 없습니다 . 코드는 <strike> tolerant </ strike>입니다. 클래스는 기본 상태로 설계되었으므로 상호 작용하는 코드가 버그를 타거나 악의적 인 정크처럼 작성되지 않고 작동합니다. 복합 부모는 유효성을 평가하기 위해 하위 계층에 도달 할 필요가 없습니다 (의미 null하여 모든 구석과 채석장에서 확인 ). 부모는 자녀의 기본 상태가 무엇을 의미하는지 알고 있습니다
radarbob
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.