契約による設計・契約プログラミングが少しワカッタ

「契約による設計 Design By Contract」とか「契約プログラミング Programming By Contract」とか、単語は聞いたことあったけど何するもんなのかよく分かんねーなーと思ってた。

Wikipedia の記事を抜粋するとこんな感じ。

プログラムコードの中にプログラムが満たすべき仕様についての記述を盛り込む事で設計の安全性を高める技法。

契約は、コードの利用条件が満たされることによって成立する。 それら条件は、満たすべきタイミングと主体によって、以下の3種類に分けられる。

  • 事前条件 (precondition)
    • サブルーチンの開始時に、これを呼ぶ側で保証すべき性質。
  • 事後条件 (postcondition)
    • サブルーチンが、終了時に保証すべき性質。
  • 不変条件 (invariant)
    • クラスなどのオブジェクトがその外部に公開しているすべての操作の開始時と終了時に保証されるべき、オブジェクト毎に共通した性質。

何かをコードに入れることでどうにかすんだろーなー、とは分かるが、何を書くことを指しているのかがよく分かっていなかった。

そしたらコチラの記事を見つけた。

とても分かりやすい。

つまりこういうことだ。

なーんだ、コレってほとんど普段から俺がやってることじゃん。と思った。アサーションは言語によっては使わないけど、関数の頭で引数チェックしてるし、関数の最後で戻り値のチェックしてるし、Setter 系の処理の所では引数チェックと同じノリでチェックしてるし。コレが契約プログラミングだったのか。

防衛的プログラミングとの違い

似たような言葉で、防衛的プログラミングという考え方もある。コチラは以下の記事で要領を掴めた。

起こりそうな例外を予めチェックしておき、例外を発生させないというプログラミング手法だ。

関数の引数チェックでは、null などの異常値を空文字に変換して処理を続行させたり。処理中の例外は catch して、何らかの初期値に差し替えて処理を終了させたり。

このようなコーディングもよくやってる。JavaScript の場合は nullundefined のような Falsy な値のチェックと変換が必要になることが多いので、そういう癖がついたと思う。

擬似コードで例示

それぞれを擬似コードで例示してみる。TypeScript 風に書くが、素の JavaScript や Java っぽい感じでも汲み取ってもらえればと。

TypeScript の場合、「その型宣言なら null はチェックせずとも弾けるのでは?」といった指摘はありうるが、実行時に TypeScript での型チェックが働くワケではないので、API から取得したデータを元に動的に処理したりしているような場面で意図した型で動作しない場合もある。あくまで擬似コードとして、言語別の個別の指摘は無視する。ココでは「何をすることが事前条件と言われるのか」といったことを押さえるために見て欲しい。

// ↓ 「不変条件」に焦点を当てた例

/** 人物を表すクラス */
class Person {
  /** 氏名 : 空文字は許容しない */
  private name: string;
  /** 年齢 : 0 以上の整数のみとする */
  private age: number;
  
  /** 氏名を設定する */
  public setName(name: string): void {
    this.name = name;  // 契約的プログラミングにおける「不変条件」(空文字を許容しない) のチェックをしていない
  }
  
  /**
   * 年齢を設定する
   * 
   * @param age 年齢。0 以上の整数のみとする
   */
  public setAge(age: number): void {
    // 契約的プログラミングにおける「事前条件」のチェック : 引数チェック
    if(age == null) throw new IllegalPreConditionException('引数 age が null か undefined です');
    if(!Number.isInteger(age)) throw new IllegalPreConditionException('引数 age が小数です');
    if(age < 0) throw new IllegalPreConditionException('引数 age が負数です');
    // 契約的プログラミングにおける「不変条件」の事前チェック : フィールドの変更前の値をチェックする・ココでは assert ではなく例外スローとしている
    if(this.age == null) throw new IllegalInvariantConditionException('処理前のフィールド age が null か undefined です');
    if(!Number.isInteger(this.age)) throw new IllegalInvariantConditionException('処理前のフィールド age が小数です');
    if(this.age < 0) throw new IllegalInvariantConditionException('処理前のフィールド age が負数です');
    
    // 実処理 : フィールドに値を設定する
    this.age = age;
    
    // 契約的プログラミングにおける「不変条件」の事後チェック : フィールドの変更後の値をチェックする・ココでは assert ではなく例外スローとしている
    if(this.age == null) throw new IllegalInvariantConditionException('処理後のフィールド age が null か undefined です');
    if(!Number.isInteger(this.age)) throw new IllegalInvariantConditionException('処理後のフィールド age が小数です');
    if(this.age < 0) throw new IllegalInvariantConditionException('処理後のフィールド age が負数です');
  }
}


// ↓ 「事前条件」「事後条件」に焦点を当てた例

/**
 * 引数の2つの値を加算する関数
 * 
 * @param numberA 数値1。整数のみとし、負数は受け取らないものとする
 * @param numberB 数値2。整数のみとし、負数は受け取らないものとする
 * @return 引数の2つの値を加算した合計値。合計値は整数のみとし、負数にはならないものとする
 * @throws 引数もしくは合計値が異常値 (小数、負数、null、undefined のいずれか) である場合
 */
function add(numberA: number, numberB: number): number {
  // 契約的プログラミングにおける「事前条件」のチェック : 引数チェック
  if(numberA == null) throw new IllegalPreConditionException('引数 numberA が null か undefined です');
  if(numberB == null) throw new IllegalPreConditionException('引数 numberB が null か undefined です');
  if(!Number.isInteger(numberA)) throw new IllegalPreConditionException('引数 numberA が小数です');
  if(!Number.isInteger(numberB)) throw new IllegalPreConditionException('引数 numberB が小数です');
  if(numberA < 0) throw new IllegalPreConditionException('引数 numberA が負数です');
  if(numberB < 0) throw new IllegalPreConditionException('引数 numberB が負数です');
  // 契約的プログラミングにおける「不変条件」がある場合は、その事前チェックをここで行う
  
  // 実処理 : 引数の2つの値を加算する
  const result = numberA + numberB;
  
  // 契約的プログラミングにおける「事後条件」のチェック : 戻り値のチェック・ココでは assert ではなく例外スローとしている
  if(result == null) throw new IllegalPostConditionException('戻り値 result が null か undefined です');
  if(!Number.isInteger(result)) throw new IllegalPostConditionException('戻り値 result が小数です');
  if(result < 0) throw new IllegalPostConditionException('戻り値 result が負数です');
  // 契約的プログラミングにおける「不変条件」がある場合は、その事後チェックをここで行う
  
  // 終了する
  return result;
}


// ↓ 呼び出し方の違いによる「防衛的プログラミング」の例

// メイン処理
function main() {
  // 「契約的プログラミング」では呼び出し元の呼び出し方については触れていない
  // なので、引数に与える値の妥当性を呼び出し元ではチェックしないでいる
  const resultA = add(1, 2);
  const resultB = add(3, 4.5);  // ← 関数内の「事前条件」違反で例外がスローされることになる
  
  
  // 一方「防衛的プログラミング」は、原則的に想定される例外は絶対に発生させないようにする
  // 例えば「計算に失敗した場合は -1 を出力する」といった例外ハンドリング処理を設計に組み込んでおく
  try {
    // 実際は API 等から値を取得するテイで…
    const numberA = 6;
    const numberB = 7.8;
    
    // 関数を呼び出す前に、呼び出し元で例外が発生しないように処理する : ココでは catch 句に移動させるため例外をスローしている
    if(numberA == null || !Number.isInteger(numberA) || numberA < 0) throw new IllegalParameterException('変数 numberA が不正値のため計算を開始できません');
    if(numberB == null || !Number.isInteger(numberB) || numberB < 0) throw new IllegalParameterException('変数 numberB が不正値のため計算を開始できません');
    
    // 安全に add() 関数を呼び出せることが分かったら実行する
    const result = add(numberA, numberB);
    
    // コンソール出力して終了する
    console.log(result);
  }
  catch(error) {
    // 事前に設計した例外ハンドリング時の処理どおり、計算に失敗した場合は -1 を出力する
    if(error instanceof IllegalParameterException    ||
       error instanceof IllegalPreConditionException ||
       error instanceof IllegalPostConditionException)
      console.log(-1);
      return;  // コンソール出力して終了する
    }
    
    // 万が一、設計時に想定できていないエラーが発生した場合のためのスロー文
    throw error;
  }
}

愚直に実装しようとすると、まぁメンドクサイなと思う気持ちは分からんでもない。ただ、

…といったメリットが考えられるので、僕は割と自然にこういう感じのコードは書いてきた。

言われなくてもやってたわ

こんなの「契約プログラミング」だの「防衛プログラミング」だの、大層な名前を付けずとも当たり前にやってることだろ、と思ったんだけど、違うのかしら。「概念を知らないと意識できない」とは思うが、僕はこれらの手法の概念を理解していなかったのに、「こうやれば色々防げるでしょ」って思い付いていた。ということは誰でも思い付く程度のことだろう、と思うんだけど、違うのかしらねぇ。