web-dev-qa-db-ja.com

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

私は次のようなコードをたくさん書いています:

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

ここでスタイルの選択についてのコメントに興味があります。 [個々のステートメントの詳細についてあまり気にしないでください。興味があるのは、全体的な制御フローです。]

27

この種の問題については、Martin Fowlerが提案しました指定パターン

...設計パターン。これにより、ブールロジックを使用してビジネスルールをチェーン化することにより、ビジネスルールを再結合できます。

仕様パターンは、他のビジネスルールと組み合わせることができるビジネスルールの概要を示します。このパターンでは、ビジネスロジックのユニットは、その機能を抽象集約複合仕様クラスから継承します。複合仕様クラスには、ブール値を返す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;
}

すべてのチェックが単一のメソッド内のプレーンリストに収まるように見えるように、ケースはかなり単純に見えます。読みやすくするには、多くのメソッドに分割する必要があります。

私は通常、「スペック」に関連するメソッドを専用オブジェクトにグループ化/抽出しますが、それがなくてもケースは問題ありません。

  // ...
  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 pattern」を提案し、Eric EvansとMartin Fowlerによって作成された "Specifications"ペーパーに言及しています。

27
gnat

検証を独自の関数に移動する方が簡単だと思います。他の関数の意図をより明確に保つのに役立つので、あなたの例は次のようになります。

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;
}
8
Ryathal

特にうまく機能しているのは、コードに検証レイヤーを導入することです。最初に、厄介な検証をすべて行い、何かがうまくいかない場合にエラー(上記の例では-1)を返すメソッドがあります。検証が完了すると、関数は別の関数を呼び出して実際の作業を行います。これで、この関数はこれらの検証手順をすべて実行する必要はありません。既に実行されているはずだからです。つまり、仕事関数仮定は入力が有効であることを意味します。どのように仮定に対処する必要がありますか?コードでそれらをアサートします。

これにより、コードが非常に読みやすくなります。検証メソッドには、ユーザー側のエラーを処理するための面倒なコードがすべて含まれています。 workメソッドは、アサーションを使用して仮定を明確に文書化し、無効である可能性のあるデータを処理する必要がありません。

あなたの例のこのリファクタリングを検討してください:

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