不変クラスはどのように良いコードに貢献するのか

by

@wapa5pow

不変クラスとは何か

前回の記事で紹介したように契約プログラミングでは以下の条件がありました。

  • 事前条件(preconditions)
  • 事後条件(postconditions)
  • 不変条件(invariants)

事前条件と事後条件は想像がつくのですが不変条件はわかりにくいです。
不変条件とはメソッドが呼ばれたときの開始時と終了時に共通して保証されるべき条件です。例えば銀行口座の場合は口座の金額がマイナスにならないなどの条件です。

この不変条件をクラスに当てはめると 「守るべき条件をクラスのライフタイム(生成時・メソッド後)で必ず満たす」 不変クラス(Class invariant)になります。

不変クラスはどのように良いコードに貢献するのか

例えば銀行口座クラスがあったとします。
Dartで一番シンプルに実装すると以下のクラスになります。

class BankAccount {
  /// 残高
  int amount = 0;
}

「残高がマイナスにならない」というルールがあったとして、不変クラスでないので様々な不安があります。

  • amountを直接変更できるのでマイナスな値に外部から変更されるかもしれない
  • amountを直接変更されるコードがあちこちに実装されamountへの検証(たとえばマイナスでない)を入れようとしたときに複数のコードを変更する必要がある

不変クラスにより不安を取り除きます。不変クラスはルールをオブジェクトのライフタイムでまもらなけばならないので以下のようにします。(様々な方法があるので一例です)。

  • 外部から変数を直接いじれないようにする
  • コンストラクタで条件を守っているか検証する
  • メソッドでプロパティの値が変更されたら必ず検証を通す

このような条件を満たすと以下のようなクラスになります。
(Dartだと先頭に_がついているとprivateなメソッドやプロパティになります。)

class BankAccount {
  /// 残高
  int _amount;
  int get amount => _amount;

  BankAccount(this._amount) {
    _validate();
  }

  /// 残高を変更する
  void changeAmount(int newAmount) {
    _amount = newAmount;
    _validate();
  }

  /// 検証
  void _validate() {
    if (_amount < 0) throw Exception('invalid amount: $_amount');
  }
}

不変クラスにしたことにより以下が達成されました。

  • オブジェクトのライフタイムで業務ルールが守られるようになった
  • 外部から不正な値により直接プロパティが変更されることがなくなった
  • 残高の変更金額がメソッドに統一され変更を加えるのが楽になった

クラスは必ず指定の条件を満たすようになり不正な状態のクラスを扱うことがなくなりました。いままでだと外部のコードがこのクラスの不変状態を守っていましたがそこに不具合が含まれないとも限りません。

このようにクラス内で限られたインタフェースを公開し検証することでクラスが正しいという状態を常に保証できるようになります。銀行口座クラスをデータベースに保存したとしても保存されている値はかならず正しい状態のオブジェクトが保存され不変クラスにより良いコードに貢献できました。

更にクラスを使いやすくする

今回はchangeAmountしかないですが複数のメソッドがあったりしたら毎回_validate()を呼ぶのが面倒です。何よりつけ忘れる可能性があります。
_amountの定義もgetterとsetterで定義しており何か冗長な感じがします。Immutableなクラスにすることでもっとシンプルにすることができます。

class BankAccount {
  /// 残高
  final int amount;

  BankAccount(this.amount) {
    _validate();
  }

  /// 残高を変更する
  BankAccount changeAmount(int newAmount) {
    return BankAccount(amount);
  }

  /// 検証
  void _validate() {
    if (amount < 0) throw Exception('invalid amount: $amount');
  }
}

コンストラクタで検証をして、さらにメソッド呼び出しの結果としてクラスを生成して返せば必ずコンストラクタを通るので不変クラスとなれます。

複数のプロパティがありいちいちコンストラクタに複数の値を入れて面倒な場合は、freezedでcopyWithというメソッドを生成してくれるのでそれを使うのもありです。

まとめ

不変クラスは何か、どのように実装するのかを説明しました。
不変クラスを使ってより安全なコードを書くことができます。業務アプリケーションでルールが複雑だと不具合を起こす確率が高くなりますがこのような不変クラスで良いコードをかければ不具合の少ないアプリケーションに貢献することができます。

参考