不変条件ってそもそも何なのか
不変条件(invariant)は
「このオブジェクトが“生きている間ずっと”守られていないといけない約束」
のことです。
例えると、
ユーザーのポイントは常に 0 以上
注文は明細を 1 件以上必ず持つ
メールアドレスは常に正しい形式
口座残高は常に 0 以上
みたいな、「このクラスが“まともな状態”であるための前提」です。
オブジェクト指向では、
「その不変条件を“コードで”どう守るか」がめちゃくちゃ重要になります。
ここをちゃんとやると、バグの入り方が目に見えて変わります。
不変条件を守れないコードの典型例
何でもアリにしてしまう User クラス
まずは「不変条件ガバガバ」な例から。
final class User {
private String name;
private String email;
private int point;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public int getPoint() { return point; }
public void setPoint(int point) { this.point = point; }
}
Javaこの User に対して、呼び出し側はこうできます。
User u = new User();
u.setName(null); // 名前が null
u.setEmail("aaa"); // メールアドレスっぽくない
u.setPoint(-100); // マイナスのポイント
Javaもし業務的には
名前は必須
メールは「@」を含む形式
ポイントは常に 0 以上
というルールがあるなら、
このクラスは「不変条件をまったく守っていない」状態です。
不変条件を守れていないと何が起きるかというと、
「どこかで u の状態がおかしくなっても、その時点では例外も何も起きない」
「ずっと後になって“あれ、なんで point マイナスなんだ?”とデバッグする羽目になる」
という泥沼にハマります。
守り方 1: コンストラクタで「おかしい状態」をそもそも作らせない(重要)
生成の瞬間に「正しさ」をチェックする
もっとも強力な守り方は、
「変なオブジェクトは、そもそも new できないようにする」ことです。
先ほどの User を、不変条件を守るように書き直します。
final class User {
private final String name;
private final String email;
private final int point;
User(String name, String email, int point) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("名前は必須です");
}
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("メール形式が不正です");
}
if (point < 0) {
throw new IllegalArgumentException("ポイントは0以上です");
}
this.name = name;
this.email = email;
this.point = point;
}
String name() { return name; }
String email() { return email; }
int point() { return point; }
}
Javaこのクラスは、不変条件をこう定義しています。
名前は null/空文字禁止
メールは必ず「@」を含む
ポイントは 0 以上
それをコンストラクタでチェックし、
条件を満たさないなら例外を投げて “そもそも作らせない”。
これで、「User のインスタンスは必ずこの条件を満たしている」と言い切れるようになります。
イミュータブルにするとさらに安心
上の例ではフィールドをすべて final にしました。
こうすると、作られたあとは状態を二度と変えられません。
一度でも不変条件を守った状態で作れていれば、
その後は「勝手に壊される心配がない」ということです。
イミュータブルにできない事情もありますが、
できるところは final にしてしまうと、不変条件の守りが一段強くなります。
守り方 2: 変更メソッドの中でルールをチェックする
setter ではなく「意味のある操作」にする
どうしても状態を変更する必要があるクラスもあります。
その場合でも「ただの setter」にせず、
「そのクラスにとって意味のある操作」としてメソッドを定義して、
その中できっちり不変条件を守ります。
さっきの User に「ポイントを増やす」という操作を足すとします。
final class User {
private final String name;
private final String email;
private int point;
User(String name, String email, int point) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("名前は必須");
}
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("メール形式が不正");
}
if (point < 0) {
throw new IllegalArgumentException("ポイントは0以上");
}
this.name = name;
this.email = email;
this.point = point;
}
void addPoint(int added) {
if (added < 0) {
throw new IllegalArgumentException("マイナス加算は禁止");
}
int result = this.point + added;
if (result < 0) { // オーバーフロー防御のための例
throw new IllegalStateException("ポイントオーバーフロー");
}
this.point = result;
}
String name() { return name; }
String email() { return email; }
int point() { return point; }
}
JavaaddPoint は、「このクラスの不変条件を壊さないようにポイントを変更する」唯一の入り口です。
呼び出し側はもう setPoint などは呼べません。
「状態を変えるメソッドの中こそ、不変条件を守るチェックを書く場所」
という意識を持っておくと、設計の軸がブレにくくなります。
守り方 3: セットで整合が必要なものは、一緒に操作する
片方だけ変えられる API は危険
例えば「注文」の例を考えます。
合計金額 totalPrice と、明細 lines があるとします。
final class Order {
private List<OrderLine> lines;
private int totalPrice;
public void setLines(List<OrderLine> lines) { this.lines = lines; }
public void setTotalPrice(int totalPrice) { this.totalPrice = totalPrice; }
}
Javaこのように「明細」と「合計金額」をバラバラに変更できるようにすると、
簡単に矛盾した状態(明細と totalPrice が合ってない)が作れてしまいます。
不変条件を守るなら、
「明細の変更と合計金額の変更はセットで行うメソッドにする」のが筋がいいです。
final class Order {
private final List<OrderLine> lines = new ArrayList<>();
void addLine(Item item, int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("数量は1以上");
}
this.lines.add(new OrderLine(item, quantity));
// 合計金額は totalPrice() メソッドで毎回計算する、などにする
}
Money totalPrice() {
Money total = Money.zero();
for (OrderLine line : lines) {
total = total.add(line.lineTotal());
}
return total;
}
}
Java「合計金額は保持せず、その都度明細から計算」
あるいは
「内部でだけ totalPrice を更新し、外から直接書き換えられないようにする」
といった工夫で、「矛盾した状態をそもそも作れない API」にします。
不変条件を守るうえで、
このフィールドとこのフィールドは、セットで一貫性がある必要があるか?
片方だけ変更できるメソッドを提供してしまっていないか?
という自問はかなり効きます。
守り方 4: 値オブジェクトに小さな不変条件を閉じ込める(重要)
int や String のままだと守りにくい
例えば「金額」「メールアドレス」「個数」などを、
そのまま int や String で持っていると、不変条件が守りにくくなります。
int price; // 0 以上? 税込? 税抜? 通貨は?
String email; // 空文字? 半角だけ? 形式チェック?
Javaこういう「意味のある値」は、小さなクラス(値オブジェクト)にしてしまうと守りやすくなります。
Money の例
final class Money {
private final int amount;
Money(int amount) {
if (amount < 0) {
throw new IllegalArgumentException("金額は0以上");
}
this.amount = amount;
}
static Money zero() {
return new Money(0);
}
Money add(Money other) {
long result = (long) this.amount + other.amount; // オーバーフロー対策例
if (result > Integer.MAX_VALUE) {
throw new IllegalStateException("金額オーバーフロー");
}
return new Money((int) result);
}
int amount() {
return amount;
}
}
Java「金額は 0 以上」という不変条件を Money の中に閉じ込めました。
これを使う側は、もう「0 未満かどうか」を毎回チェックしなくて済みます。
Money price = new Money(1000);
Money total = price.add(new Money(500));
new Money(-100) をしようとすれば、その場で例外になります。
「金額としてありえない状態」はシステムに入り込めません。
このように、不変条件を持った小さなクラスをたくさん作ると、
「システム全体の安全性」がじわじわ効いてきます。
守り方 5: 不変条件をテストコードで“言語化”する
「このクラスの不変条件は何か」をテストに落とす
不変条件はコードの中でチェックするだけでなく、
テストコードとして「目に見える形」にしておくと強いです。
User の例なら、こんなテストが考えられます。
void 名前が空なら例外() {
assertThrows(IllegalArgumentException.class,
() -> new User("", "a@example.com", 0));
}
void ポイントがマイナスなら例外() {
assertThrows(IllegalArgumentException.class,
() -> new User("Taro", "a@example.com", -1));
}
void addPointでマイナスを渡したら例外() {
User u = new User("Taro", "a@example.com", 0);
assertThrows(IllegalArgumentException.class,
() -> u.addPoint(-10));
}
Javaこれを見れば、「このクラスが守るべき不変条件」がテスト名として一目で分かります。
不変条件は「何となく暗黙の了解」で済ませると、
いつの間にか誰かが破り始めます。
テストという形で「このクラスはこうあるべき」と明文化しておくと、
後から仕様を変えるときも安心して動けます。
まとめ:不変条件を守る設計かどうかをチェックする視点
不変条件の守り方は、要するにこういうことです。
作る瞬間(コンストラクタ)でおかしな状態を拒否できているか
状態を変えるメソッドの中で一貫性を壊していないか
セットで整合性が必要なフィールドをバラバラに変えられていないか
int や String のまま放置している「意味のある値」はないか
テストで「このクラスのルール」を表現できているか
どれか一つでも意識し始めると、設計の感覚がかなり変わります。
