これまで読んできたプログラミング本の中では、Joshua Bloch 著書の「Effective Java」が未だに印象に残っています。Java を中心にした内容ですが、オブジェクト指向プログラミング全般に当てはめられるアドバイスが詰まっています。この本を読んで、「クラス不変条件」という概念を初めて知りました。不変条件によって排除される、不可能なオブジェクト状態に対して、防御的なコードを書かなくても良いと。本の中では、以下の Java コードがその例として挙げられています。

public class Favorites {
    private Map<Class<?>, Object> favorites = new HashMap<>();

    public <T> void putFavorite(Class<T> type, T instance) {
        favorites.put(Objects.requireNonNull(type), instance);
    }

    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
    }
}

厳密に言えば、引数の型が間違っていれば、type.castは例外をスローしますが、それでもgetFavoriteは絶対に例外をスローしないと断言できます。なぜなら、favoritesはクラス不変条件を守っています:キーが型のトークンで、値がその型のインスタンスです。よって、例外をスローしない(T)よりも、もし不変条件がバグが原因で守られなかった場合、呼び出し元じゃなくクラス内から例外をスローするtype.castが採用されています。少々非現実的な例ではありますが、様々なプロジェクトに関わってきて僕が見てきた事実を示しています:防御的プログラミングを良しとしないプロジェクトの方がバグが少ないです。

「クラス不変条件」は表現としてよく使われますが、クラスの規模を超えて、複数のクラスを含むプログラム、あるいは複数のプログラムを含むシステムとなると、「不変条件」という表現が目に入らなくなります。が、不変条件が当てはまらなくなるというわけではなく、それを示す用語が変わってくるだけです。「メソッドの契約」、「API の契約」、「アーキテクチャー」、「チームの合意」が全て、ある種の不変条件を示しています。

まだ記憶に残っている例が挙げられます。アプリを設定として、一つのデータのブロブを採用していたプロジェクトの話です。そのデータの塊が小さなエントリーによって構成されており、それぞれのエントリーにはそれを参照するための ID が付いていました。そういった ID が、他のエントリーだったり、データベースだったり、システム全体に散らばっていました。ところが、チーム全員で「ID の参照先のエントリーが絶対に存在する」と合意したため、アプリケーションコードのほとんどが、ID の参照先が正しいを前提として書かれていました。「ほとんど」というのは、壊れた参照を意識せざるを得ない、不変条件を守るためのシステムの一部があったからです。おかげで、それ以外のコードは、不変条件の恩恵を受けることができました。

結果として、以下のようなコードを書くことができました:

class App {
    private final AppData data;

    // ...

    void doSomethingWithData() {
        var a = data.getThingA();
        var b = data.getThingB();
        var c = b.getThingCByIdOfA(a.getId());
        // cをnullチェックしないでそのまま使います
        // ...
    }
}

ちなみに、不変条件がなかったら、以下のようなコードになってしまいます:

class App {
    private final AppData data;

    // ...

    void doSomethingWithData() {
        var a = data.getThingA();
        var b = data.getThingB();
        var c = b.getThingCByIdOfA(a.getId());
        if (c == null) {
            // 不変条件がないため、nullの可能性に対応せざるを得ません
        } else {
            // 実際のアプリの動作はここです
        }
    }
}

nullチェックは別にいいんじゃないかと思われるかもしれませんが、開発者全員が何百回も同じ対応を重ねると、少々よろしくない状況が生まれてきます:

  • 不変条件で排除することができた問題に何百回も対応しなければなりません。
  • 可視化されていない対応策が選択されたら、データの不整合が気づかれない恐れがあります。
  • 対応策を書くための時間が費やされます。コードが余計に多くなり、読みにくくなります。

防御的プログラミングを一方的に否定するつもりはありませんが、不変条件によって置き換えられる防御的プログラミングを避けた方が良いんじゃないかと思っています。そうすれば、どの不変条件を、どう守れば良いのか、という組織やカルチャーの問題に絞られるでしょう。