유효성 검사를 통한 제어 흐름 스타일


27

나는 다음과 같은 많은 코드를 작성하고 있음을 발견했다.

int myFunction(Person* person) {
  int personIsValid = !(person==NULL);
  if (personIsValid) {
     // do some stuff; might be lengthy
     int myresult = whatever;
     return myResult;
  }
  else {
    return -1;
  }
}

특히 여러 검사가 관련된 경우에는 매우 혼란 스러울 수 있습니다. 그런 경우 다음과 같은 대체 스타일을 실험했습니다.

int netWorth(Person* person) {
  if (Person==NULL) {
    return -1;
  }
  if (!(person->isAlive))  {
    return -1;
  }
  int assets = person->assets;
  if (assets==-1)  {
    return -1;
  }
  int liabilities = person->liabilities;
  if (liabilities==-1) {
    return -1;
  }
  return assets - liabilities;
}

스타일 선택에 대한 의견에 관심이 있습니다. [개별 진술의 세부 사항에 대해 너무 걱정하지 마십시오. 관심이있는 것은 전체적인 제어 흐름입니다.]


8
귀하의 예에서 상당히 심각한 사양 오류가 있음을 알려 드리겠습니다. 예를 들어 자산 == 42 및 부채 == 43 인 경우 해당 개인을 존재하지 않는 것으로 선언합니다.
John R. Strohm

예외가 발생하지 않고 클라이언트 코드가 유효성 검사를 더 잘 관리하도록 하시겠습니까?
Tulains Córdova

@ TulainsCórdova 예외를 사용할 수 없거나 유효하지 않은 데이터가 스택 추적 등을 구축 할 때의 성능 영향을 충분히 수용 할 수있을 정도로 예외적 인 것은 아닙니다.
헐크

답변:


27

이러한 종류의 문제에 대해 Martin Fowler는 다음과 같이 사양 패턴을 제안했습니다 .

부울 논리를 사용하여 비즈니스 규칙을 연결하여 비즈니스 규칙을 다시 결합 할 수있는 디자인 패턴.
 
사양 패턴은 다른 비즈니스 규칙과 결합 할 수있는 비즈니스 규칙을 설명합니다. 이 패턴에서 비즈니스 단위 (UOW) 논리는 추상 집계 복합 스펙 클래스에서 해당 기능을 상속합니다. Composite Specification 클래스에는 부울 값을 반환하는 IsSatisfiedBy라는 함수가 있습니다. 인스턴스화 후 사양은 다른 사양과 "연쇄"되어 새 사양을 쉽게 유지 관리 할 수 ​​있지만 사용자 지정할 수있는 비즈니스 로직으로 만듭니다. 또한 인스턴스화시 비즈니스 로직은 메소드 호출 또는 제어 역전을 통해 지속성 저장소와 같은 다른 클래스의 대리자가되기 위해 상태를 변경할 수 있습니다.

위의 소리는 눈에 띄게 들리지만 (적어도 나에게는) 내 코드에서 시도했을 때 매우 매끄럽게 구현하고 쉽게 구현하고 읽을 수있는 것으로 나타났습니다.

내가 보는 방식으로, 주요 아이디어는 검사를 전용 메소드 / 객체로 수행하는 코드를 "추출"하는 것입니다.

당신과 함께 netWorth예를 들어,이 다음과 같이 대해 볼 수 있었다 :

int netWorth(Person* person) {
  if (isSatisfiedBySpec(person)) {
    return person->assets - person->liabilities;
  }
  log("person doesn't satisfy spec");
  return -1;
}

#define BOOLEAN int // assuming C here
BOOLEAN isSatisfiedBySpec(Person* person) {
  return Person != NULL
      && person->isAlive
      && person->assets != -1
      && person->liabilities != -1;
}

모든 검사가 단일 방법 내에서 일반 목록에 적합하도록보기에 다소 단순 해 보입니다. 더 잘 읽을 수 있도록 더 많은 방법으로 분할해야하는 경우가 종종 있습니다.

또한 일반적으로 전용 객체에서 "spec"관련 메소드를 그룹화 / 추출하지만 케이스가 없으면 괜찮습니다.

  // ...
  Specification s, *spec = initialize(s, person);
  if (spec->isSatisfied()) {
    return person->assets - person->liabilities;
  }
  log("person doesn't satisfy spec");
  return -1;
  // ...

Stack Overflow의이 질문은 위에서 언급 한 것 외에도 몇 가지 링크를 권장합니다 : Specification Pattern Example . 특히, 답변은 Dimecasts의 '사양 학습 패턴'에 대한 예제를 제공 하고 Eric Evans와 Martin Fowler가 작성한 "사양"문서를 언급 합니다.


8

유효성 검사를 자체 함수로 옮기는 것이 더 쉽다는 것을 알았습니다. 다른 함수의 의도를보다 깨끗하게 유지하는 데 도움이되므로 예제는 다음과 같습니다.

int netWorth(Person* person) { 
    if(validPerson(person)) {
        int assets = person->assets;
        int liabilities = person->liabilities;
        return assets - liabilities;
    }
    else {
        return -1;
    }
}

bool validPerson(Person* person) { 
    if(person!=NULL && person->isAlive
      && person->assets !=-1 && person->liabilities != -1)
        return true;
    else
        return false;
}

2
왜해야합니까 if에서를 validPerson? person!=NULL && person->isAlive && person->assets !=-1 && person->liabilities != -1대신에 반납하십시오 .
David Hammen

3

내가 특히 잘 보았던 것 중 하나는 코드에 유효성 검사 계층을 도입하는 것입니다. 먼저 모든 지저분한 유효성 검사를 수행하고 문제가 발생하면 오류를 반환하는 메서드가 있습니다 ( -1위의 예와 같이). 유효성 검사가 완료되면 함수는 다른 작업을 호출하여 실제 작업을 수행합니다. 이제이 기능은 이미 완료되었으므로 모든 유효성 검사 단계를 수행 할 필요는 없습니다. 즉, 작업 함수 입력이 유효 하다고 가정 합니다. 가정을 어떻게 다루어야합니까? 당신은 코드에서 그것들을 주장한다.

이것이 코드를 매우 쉽게 읽을 수 있다고 생각합니다. 유효성 검사 방법에는 사용자 측의 오류를 처리하기위한 모든 지저분한 코드가 포함됩니다. 작업 방법은 가정 정보를 가정하고 문서화 한 다음 유효하지 않은 데이터로 작업 할 필요가 없습니다.

예제의 리팩토링을 고려하십시오.

int myFunction(Person* person) {
  int personIsValid = !(person==NULL);
  if (personIsValid) {
     return myFunctionWork(person)
  }
  else {
    return -1;
  }
}

int myFunction(Person *person) {
  assert( person != NULL);  
  // Do work and return
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.