🔥

契約による設計の解釈

2023/11/29に公開

契約による設計とは

利用側と対象の間に「特定の保証を受ける権利とそのために守る義務=契約」があるとみなすこと。
この条件を守らないと対象の操作をしないよ!(事前条件)、その代わり対象の操作が終わった時点である条件を保証するよ!(事後条件)という設計。

問題の発生

こんなコードを考える。

public class Mob {
    private String name;
    private int hp;
    public Mob(int hp) {
        this.hp = hp;
    }
    public void damage(int point) {
        this.hp -= point;
    }
}

するとこんなことが起きうる。

// 最初MobがHP100だったとして。。。
this.mob.damage(-50); // HP = 150, ダメージ与えたはずなのに。。。

解決手法と解釈 : 事前条件の利用とその表明

きっとこのクラスの作者はこんな使われ方するって思ってなかっただろう。
だったらどうする?コメントでもつけておく?
コメントより実効する命令を足してみるべきだろう。

    public void damage(int point) {
	if(point > 0) throw new RuntimeException();
        this.hp -= point;
    }

これで再度実行すると。。。

// 最初MobがHP100だったとして。。。
this.mob.heal(-10); // マイナスなのでError、HPは引かれない
this.mob.damage(-50); // マイナスなのでError、HPは引かれない

一番やりたい処理であるthis.hp -= point;の前にpointの条件を明記する。
これが事前条件とその表明。

解決手法と解釈 : 事後条件の利用とその表明

ただし。。。

// 最初MobがHP100だったとして。。。
this.mob.heal(10); // HP = 110
this.mob.damage(130); // HP = -20。。。HPがマイナスって?

ダメージがHPを超えることは往々にしてある。だがそれは実際にHPを引いてみないとわからない。
ダメージも条件を満たしていてHPを引くということも問題ない処理のはずだ。
だけどHPは0を下回ってしまう。HPは0より下にはなれない。
この場合には何が悪いのか?端的には「HPがマイナスになるケースが考慮されてない」ことにある。
HPの持ち主はMobなので、この場合HPがマイナスになるかどうかのチェックはMobの責任になる。
つまり

    public void damage(int point) {
	if(point > 0) throw new RuntimeException();
        this.hp -= point;
	if(this.hp < 0) this.hp = 0;
    }

このif(this.hp < 0) this.hp = 0;が事後条件。
実際にやりたいことをやった後に、ありえない状態にならないことを担保する条件とその表明。

事前条件・事後条件を使うとどうなる?

事前条件に違反するとまずHPが引かれない。処理が実行されない。逆説的に「守られた場合のみ実行される」が確実になる。
また、何かしら利用されるものを作る場合に役立つ。事前に表明しておけば内部では値が保証されてることを確実に信頼できる。

事後条件は値をある範囲内に担保する。利用する側にとっては安心だ。利用後に操作結果を検証しないで済む。

クラスへの拡張 : 不変条件

ダメージを与えられたときにHPがマイナスにならないことは先の事前条件及び事後条件で担保できた。
ただMobが利用されるときにはまず生成される。このときにHPがマイナスになることも当然論理的にはあり得る。
ただHPはマイナスになれない。だがHPがマイナスになったときに糾弾されるのは持ち主であるMobだ。
これを防ぐため、コンストラクタにも事前条件及び事後条件を適用する。

public class Mob {
    private String name;
    private int hp;
    public Mob(int hp) {
        if(hp < 0) throw new RuntimeException();
        this.hp = hp;
	if(this.hp < 0) this.hp = 0;
    }
    public void damage(int point) {
	if(point > 0) throw new RuntimeException();
        this.hp -= point;
	if(this.hp < 0) this.hp = 0;
    }
}

これで、MobのHPにはメソッドとコンストラクタ経由でのみアクセス可能で、かつ全てのメソッドでHPが0にならない状態を強制できた。これが不変条件及びその表明。

不変条件を満たされるとHPが0にならない。Mobにどれだけ傷を負わせても大丈夫だ。

これで絶対安心だ!と思いきや。。。

実際のケースでは確実に安心だとは言えない。
例えばダメージを与える処理ではなく、DBから引くケースなどでは実行時のSQLExceptionを考慮する必要がある。

public List<Mob> findByName(String name) {
    if (name.isEmpty()) throw new RuntimeException();
    List<Mob> result = ... // 何らかのSQL処理
    if(result.length() > 0) result = new ArrayList<>();
}

端的にはプログラムの責任の範囲外。事前事後条件を守っているにも関わらず。。。
この場合には当たり前だけど例外を投げる。例外を投げると当たり前だけど以降の処理は止まる。
契約的には放棄された扱いになる。
実は今までしれっとRuntimeExceptionを投げていたのもこれに起因する。
事前事後不変条件は、それが守られなかったケースでは利用側と対象のいずれかが悪い。よってプログラムが悪い=バグ=RuntimeExceptionをthrowする。

別記事でじっくり考えてみようと思うが、javaのassertを使わない理由もこのへんにあるのではと思う。
assertを使うと投げる例外を制御できない。AssertionErrorはErrorなので判断が難しいが少なくともバグで投げていいような気軽なものではなさそうだ。

まとめ

契約による設計=「特定の保証を受ける権利とそのために守る義務=契約を考慮し設計する」

事前条件

=利用側がある条件を守らないと操作されない
=利用者は条件を守る義務がある
=対象は想定外の状況で呼ばれない保証を得る
=対象は想定外の入力が怖くない

事後条件

=対象が操作終了時にある条件を保証する
=利用側は対象の操作結果に想定外が起きえない保証、事前定義済みの特性を得る
=利用側は操作結果の想定外が怖くない

不変条件

=特定のモジュールがそのライフサイクルの間で不変である義務を負う
=利用者は特定のモジュールの変化が怖くない

Discussion