カプセル化の基本と実践
📋 この記事の目次
前回、TicketCraftのTicketクラスがすべてpublicフィールドで作られていることを確認しました。今回はここに手を入れます。実は先週、こんな事故がありました。
Ticket ticket = new Ticket();
ticket.status = "wip"; // 本来は "in_progress" が正しい表記だった
statusに本来と違う文字列が入ってしまい、「対応中のチケットを通知する」バッチ処理がこのチケットだけ検知できず、対応漏れが発生しました。原因は単純です。statusフィールドがpublicで、誰でもどんな文字列でも直接代入できてしまうからです。
カプセル化は「受付を1つに絞る」ことだと考える
会社の資料室を想像してください。誰でも自由に出入りして棚の書類を書き換えられる資料室と、受付を1つ通してからでないと書類にアクセスできない資料室、どちらが安全でしょうか。
カプセル化とは、まさにこの「受付を1つに絞る」仕組みです。
- データ(フィールド)は
privateにして直接触れないようにする - データを読み書きしたいときは、決められた窓口(getter/setter)を必ず通す
- 窓口では「その値は本当に正しいか」をチェックできる(バリデーション)
実践:TicketCraftのTicketクラスをカプセル化する
Step 1: フィールドをprivateにする
public class Ticket {
private String id;
private String title;
private String status;
private String assignee;
}
この時点で、外部からticket.status = "wip";のように直接書き換えることはできなくなります(コンパイルエラーになります)。
Step 2: getterを用意する
public class Ticket {
private String id;
private String title;
private String status;
private String assignee;
public String getId() { return id; }
public String getTitle() { return title; }
public String getStatus() { return status; }
public String getAssignee() { return assignee; }
}
読み取り専用の情報(例えばidは一度発行したら変わらない)は、getterだけを用意してsetterを作らない、という選択もできます。これも立派なカプセル化です。
Step 3: setterにバリデーションを持たせる
ここが今回の事故を防ぐ本丸です。statusに自由な文字列を許さず、決められた値だけを受け付けるようにします。
public class Ticket {
private static final Set<String> VALID_STATUSES =
Set.of("open", "in_progress", "resolved", "closed");
private String status = "open";
public String getStatus() {
return status;
}
public void setStatus(String newStatus) {
if (!VALID_STATUSES.contains(newStatus)) {
throw new IllegalArgumentException("不正なステータスです: " + newStatus);
}
this.status = newStatus;
}
}
こうしておけば、ticket.setStatus("wip");と書いた瞬間に例外が発生し、その場でミスに気づけます。本番環境で静かにバグを埋め込むより、開発中に大きな音を立ててエラーになってくれる方がずっと親切です。
※実務では、こうした「決まった値しか許さない」フィールドには文字列のSetではなくEnum(列挙型)を使うのが王道です。ここでは基本の考え方に集中するためStringのままにしていますが、次のステップとしてEnumも調べてみてください。
Step 4: 不変条件を保護する
「担当者(assignee)が空文字のチケットは存在してはいけない」というルールがあるなら、setterの中で守ります。
public void setAssignee(String assignee) {
if (assignee == null || assignee.isBlank()) {
throw new IllegalArgumentException("担当者は必須です");
}
this.assignee = assignee;
}
このように「そのオブジェクトが常に満たしているべき条件(不変条件)」を、setterの中でまとめて守るのがカプセル化の重要な役目です。
🧓 ベテランの現場コラム 「private化してgetter/setterを付ければカプセル化完了」と思われがちですが、それは半分だけ正解です。本当に重要なのは、setterの中に 「このオブジェクトが絶対に守るべきルール」 を書き込むことです。ただ機械的にgetter/setterを生成するだけでは、public フィールドを回りくどく書いているのと大差ありません。
こんな場面で使う/使わない
- 使う: 複数の場所から読み書きされるデータで、値の正しさを保証したい場合(TicketCraftの
statusはまさにこれ) - 使いすぎに注意: 一度作ったら二度と変更しない値(例: 発行済みのid)にまでsetterを用意してしまうと、逆に「変更してはいけないもの」まで変更できる余地を残してしまう
🧓 ベテランの現場コラム IDEの「getter/setterを自動生成」機能はとても便利ですが、全フィールドに機械的にsetterを生成してしまう新人をよく見ます。「このフィールドは本当に外から変更されるべきか」を1つずつ自問する習慣をつけると、後から「なぜここが自由に書き換えられる設計になっているのか」と悩まずに済みます。
挫折ポイントTOP3と対処法
1. getter/setterを付けただけで満足してしまう
バリデーションを書き忘れると、public フィールドと実質的に変わりません。「このsetterは何を防いでいるか」を必ず一言で説明できる状態にしましょう。
2. すべてのフィールドに機械的にgetter/setterを用意してしまう
本当に外部から変更してよい値かを都度検討しましょう。読み取り専用でよいものはgetterだけにします。
3. バリデーションのロジックを呼び出し側に書いてしまう
「呼び出し側でstatusをチェックしてからsetStatusを呼ぶ」ようにしてしまうと、チェック漏れの呼び出し元が1つでもあれば事故が起きます。チェックはオブジェクト自身(setter)の中に閉じ込めるのが鉄則です。
この先、何を学べばいいか
Ticketクラスは安全になりましたが、まだ「バグ報告」「機能要望」をtypeフィールドの文字列で区別し、呼び出し側で分岐する設計のままです。次回は継承で、チケットの種類ごとに専用のクラスを作り、この分岐を解消していきます。
まとめ
- カプセル化は「受付を1つに絞る」ことで、データの安全性を保証する仕組み
- private化+getter/setterは手段であり、目的は「不変条件をコード自身に守らせる」こと
- すべてのフィールドに機械的にsetterを用意しない。本当に変更されるべき値かを毎回考える
🎓 理解度テスト
学んだ内容を確認しましょう。全5問。