検証チェック付きの制御フローのスタイル


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の場合、その人物は存在しないと宣言します。
ジョンR.ストローム

例外をスローして、クライアントコードに検証を管理させたほうが良いのではないでしょうか?
Tulainsコルドバ

@TulainsCórdova例外を利用できない場合や、無効なデータが例外として十分でないために、スタックトレースの構築などのパフォーマンスへの影響が許容される場合があります。
ハルク

回答:


27

この種の問題について、Martin Fowlerは仕様パターンを提案しました。

...設計パターン。ブールロジックを使用してビジネスルールをチェーン化することにより、ビジネスルールを再結合できます。
 
仕様パターンは、他のビジネスルールと組み合わせることができるビジネスルールの概要を示します。このパターンでは、ビジネスロジックのユニットは、その機能を抽象集約Composite Specificationクラスから継承します。Composite Specificationクラスには、ブール値を返すIsSatisfiedByという関数が1つあります。インスタンス化後、仕様は他の仕様と「連鎖」し、新しい仕様を簡単に保守可能にする一方で、高度にカスタマイズ可能なビジネスロジックを作成します。さらに、インスタンス化の際、ビジネスロジックは、メソッドの呼び出しまたは制御の反転により、永続リポジトリなどの他のクラスのデリゲートになるために状態を変更できます...

上記は少々高額に聞こえますが(少なくとも私には)、コードで試してみると非常にスムーズになり、実装と読み取りが簡単になりました。

私の見方では、主なアイデアは、チェックを専用のメソッド/オブジェクトに行うコードを「抽出」することです。

あなたの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のこの質問では、上記の仕様パターンの例に加えて、いくつかのリンクを推奨しています 。特に、回答では、例のウォークスルーのためにDimecastsの「Learning the Specificationパターン」を提案し、Eric EvansとMartin Fowlerが執筆した「Specifications」ペーパーに言及しています。


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代わりに単に戻ります。
デビッドハメン

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.