MIS.W 公式ブログ

早稲田大学公認、情報系創作サークル「早稲田大学経営情報学会」(MIS.W)の公式ブログです!

【設計の話をすると約束したな】AdventCalendar11日目【あれは嘘だ】

48代のtomiokaです。 最近Common Lispに感動したのでそれをおすそ分けしようかとも思ったのですが、冷静になってみると自分の文才では凄さが全く伝わらないので諦めました。とりあえずOn LispとLand of LispとLet Over Lambdaをお勧めしておきます。

というわけで、自分からは契約プログラミングという考え方をご紹介します。設計に関しての話をしようとしたのですが、どうせなら勉強会開いてやったほうがいいかなと思って変えました。無計画すぎるぜ……

プログラムのバグ

プログラムにはバグが付き物です。これに異存がある人はいないでしょう。それでは、なぜバグは発生するのでしょうか? 大きく分けて、二つの原因があると言われています。

  1. プログラムのロジックに誤りがある。
  2. ユーザが不正な入力をした。

これらのバグの発生を抑える、もしくは発生した時に原因を明らかにするための手法の一つが契約プログラミングもしくは契約による設計と呼ばれるものです。

小難しい学術的な用語や詳細が知りたい人にはGoogle先生にでも聞いていただくとして(それ言ったらもうこの記事の意味ないんですけど)、なるべくふんわりざっくり実例を見ながらご紹介しましょう。

実例:移動処理

例えばローグのように上下左右にキャラクターが動くゲームを考えてみましょう。

    void Character::moveTo(int x, int y) {
        this.x = x;
        this.y = y;
    }

このプログラムではx, yの値がマップの境界の外に行ってしまうとバッファオーバランを起こす可能性があります。そこで、このプログラムを書いた人は次のようなコメントを付け、さらに企画員にこのメソッドを使うときはこの決まりに従うように言いました。

    // 0 <= x < MAP_WIDTH, 0 <= y < MAP_HEIGHTの間の数値以外与えないこと
    void Character::moveTo(int x, int y) {
        this.x = x;
        this.y = y;
    }

しかし、そんなものでバグに勝てるはずもなく、結局境界外の値を与えている箇所があったせいでプログラムがストップしているようです。バグの原因がこのメソッドの誤った使用によるということを見つけるためにひどく時間がかかりました。

このように、プログラムが暗黙に満たしているべき条件に反していた場合に自動で検出する機能があると便利ですよね? この辺りは言語によって違いますが、ほとんどの言語にはアサーションと呼ばれる機能が用意されています。

例えばC及びC++には、assertという関数があります。この関数を使うと、先ほどの関数は次のように書き直せます。

    #include <cassert> // C++. Cでは<assert.h>
    void Character::moveTo(int x, int y) {
        assert(0 <= x && x < MAP_WIDTH);
        assert(0 <= y && y < MAP_HEIGHT);
        
        this.x = x;
        this.y = y;
    }

なにやらけったいな式が二行追加されています。assert関数の中には、プログラムが満たすべき条件を入れておきます。そうすると、この条件式が偽の場合にプログラムがストップし、デバッガで捕捉することができます。

このassertを使った条件の表明の良いところは、プログラムに余計なコメントをつける必要が無く、コードの意図が明確になるところです。やっていることは単なる条件分岐ですから、if文と例外を使って同様の機能を実現することもできます。しかし、その場合はそのif文が必要な条件分岐なのかデバッグ用のチェックなのか判別しづらく、結果としてコードの意図が分かりにくくなってしまいます。

さらに、大半の実装ではassert関数はリリースビルド時には実行コードに埋め込まれないため、製品のパフォーマンス低下を気にする必要がありません。これはif文と比べてassert関数がデバッグ用として特に優れている点です。

もちろん、assert関数はどこに入れても構わないので、様々な状況で使うことができます。

    #include <cassert> // C++. Cでは<assert.h>
    void Player::shot(Bullet::Type type) {
        switch(type) {
            case MISSILE:
                ...
                break;
            case LASER:
                ...
                break;
            ...
            default:
                assert(false); // cannot be here!
        }
    }

うまく使えば便利なassert関数ですが、濫用は危険です。特に、冒頭で紹介した二つのバグの原因のうち、「ユーザの不正な入力」に対して使う場合は注意する必要があります。基本的にassertはデバッグ用でしかありません。製品において想定されるユーザの入力に対してはしっかりif文を使ってエラーメッセージを表示したり再入力を促すなどして、「親切な」プログラムを作れるように心がけましょう。

(エラーチェックなんて書いた記憶ほとんどないけどね)