torutkのブログ

ソフトウェア・エンジニアのブログ

契約による設計(Design by Contract)の実装

契約による設計(Design by Contract)の実装を検討 - torutkのブログで契約による設計の実装を検討しました。今回は、検討に従って実際にC++でプログラムコードに表してみます。

事前条件

C言語標準マクロ assert を用いた実装

C言語の標準マクロassertを使った事前条件検出と通知のコード例です。コンパイル時にNDEBUGシンボルを定義しなければ、事前条件違反時にプログラムを停止させます。

void Person::setAge(int age) {
    assert(0<=age && age<=200);  // 年齢有効範囲は0才〜200才
    ... 処理
}
C++例外機構を用いた実装

例外機構で実装すると、検出箇所(ファイル名・行番号)やメッセージを例外クラス生成時に指定する必要があります。また条件判定に普通にC++の条件文で記述するため少しコードが煩雑です。

  • 検出箇所を保持する例外クラス
class PreConditionException : public std::exception {
public:
    PreConditionException(const char* file, int line, const char* message);
    virtual ~PreConditionException() throw();
    virtual const char* what() const throw();
};
  • 事前条件を制御構文(以下の例ではif文)で記述し、事前条件違反を検出するとファイル名・行番号と条件式文字列を引数に渡します。
void Person::setAge(int age) {
    if (age < 0 || 200 < age) {
        throw PreConditionException(__FILE__, __LINE__, "0<=age<=200");
    }
    ... 処理
}
自前でマクロと例外を組み合わせた実装

契約による設計のための自前のマクロDBC_REQUIREと、契約による設計の事前条件例外PreConditionExceptionを以下のように定義します。マクロはコンパイル時に取り除けるよう#ifdef DBCで制御します。例外は

#ifdef DBC
#define DBC_REQUIRE(condition) \
    if (!(condition))
        throw PreConditionException(__FILE__, __LINE__, #condition)
#else
#  define DBC_REQUIRE(condition)
#endif

上記のマクロと例外を使用して、事前条件の表明を記述します。ずいぶんすっきりします。

void Person::setAge(int age) {
    DBC_REQUIRE(0<=age && age<=200);
    ... 処理
}

事後条件

C言語標準マクロ assert を用いた実装
void Person::setAge(int age) {
    ... 処理
    assert(age_ == age); // 事後条件は引数で指定した年齢がメンバー変数age_と一致
    return;
}
C++例外機構を用いた実装
void Person::setAge(age) {
    ... 処理
    if (age_ != age) {
        throw PostConditionException(__FILE__, __LINE__, "age_!=age");
    }
    return;
}
自前でマクロと例外を組み合わせた実装
#ifdef DBC
#define DBC_ENSURE(condition) \
    if (!(condition))
        throw PostConditionException(__FILE__, __LINE__, #condition)
#else
#  define DBC_ENSURE(condition)
#endif
void Person::setAge(int age) {
    ... 処理
    DBC_ENSURE(age_ == age);
    return;
}

クラス不変条件

クラス不変条件は、クラスのメンバ関数として定義し、メンバ関数の事前条件判定時およびメンバ関数終了時(途中で例外や復帰での終了も含まれる)にチェックします。チェック箇所が多いので、抜けがないようRAIIイディオムを使います。

クラス不変条件のメンバ関数定義

RAIIで不変条件をチェックするクラスを以下に定義します。汎用化するために、テンプレートを用いています。

template <typename T>
class Invariant {
public:
    Invariant(const T& target) : target_(target) {
        target_.assertInvariant();
    }
    ~Invariant() {
        target_.assertInvariant();
    }
private:
    const T& target_;    
};

クラス不変条件を設けるクラスは、上記RAIIでチェックするためassertInvariant()メンバ関数を定義します。
このメンバ関数の中では、C言語マクロassertを使用するなり、例外を使用するなり、独自マクロを使用するなり、随意にクラス不変条件の表明を記述します。

以下はassertを使用したクラス不変条件の表明の例です。

class Person {
    ...
    void assertInvariant() const;
};

void Person::assertInvariant() const {
    assert(name_ != 0 && id != 0);
}

あとは、各メンバ関数で不変条件チェックを行います。

void Person::setAge(int age) {
    Invariant<Person>(*this) inv; // RAIIによるクラス不変条件実行の仕込み
    DBC_REQUIRE(0<=age && age<=200); // 事前条件表明
    age_ = age;
    DBC_ENSURE(age_ == age); // 事後条件表明
    return; // 復帰時にRAIIのクラス不変条件チェックが実行
}

課題・応用

  • 継承関係にあるクラスでは、基底クラスの事前条件・事後条件・クラス不変条件も加味するべきです。
  • 事後条件をRAIIで実現するときは、契約による設計の対象メンバ関数1つにつき1つの事後条件判定関数を作成するのは手間です。