5日目のゴール
5日目のテーマも「直接触らせない」=カプセル化。
ただし今日は、ここまでの
private で隠すgetter / setter を選んで出す
不変条件をクラスの中に閉じ込める
意味のあるメソッドで外と会話する
を一段まとめて、
「カプセル化された“まともなクラス”とはどんな姿か」 を
はっきりイメージできるようになるところまで行きます。
いいクラスと悪いクラスの“空気感”をつかむ
「なんでも屋クラス」と「ちゃんとしたクラス」
ここまでの話を一度、感覚レベルに落としてみます。
悪いクラスは、だいたいこんな空気をまとっています。
フィールドが public か、getter / setter で丸出し。
どこからでも好き勝手に中身をいじれる。
ルールは外側のクラスがバラバラに守っている。
逆に、いいクラスはこうです。
フィールドは private。
外から見えるのは「意味のある操作」だけ。
ルールや不変条件はクラスの中に閉じ込められている。
5日目は、この「空気感」を
コードレベルで見分けられるようになるのがゴールです。
例題1:悪い「User」と、良い「User」
まずは“なんでも触れる User”
あえて悪い例から。
public class User {
public int id;
public String name;
public int age;
}
Javaこの User は、どこからでもこう書けます。
User u = new User();
u.id = -1;
u.name = null;
u.age = -100;
Javaルールはゼロ。
「User らしさ」を守る人が誰もいません。
この状態だと、
どこか別のクラスが「年齢は 0 以上にしようね」とか
「ID はマイナスにしないでね」とか
バラバラに気をつける必要が出てきます。
つまり、
「User のルールを User 以外が背負わされている」 状態です。
ちゃんとカプセル化された User
同じ User を、ここまで育ててみます。
public class User {
private final int id;
private String name;
private int age;
public User(int id, String name, int age) {
if (id <= 0) {
throw new IllegalArgumentException("ID は 1 以上");
}
if (age < 0) {
throw new IllegalArgumentException("年齢は 0 以上");
}
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("名前は必須");
}
this.id = id;
this.name = name;
this.age = age;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public void rename(String newName) {
if (newName == null || newName.isBlank()) {
throw new IllegalArgumentException("名前は必須");
}
this.name = newName;
}
public int getAge() {
return age;
}
public void haveBirthday() {
this.age += 1;
}
}
Javaここでやっていることを、あえて言葉にするとこうです。
ID はコンストラクタでだけ決める。
ID は 1 以上というルールを User 自身が守る。
名前は必須で、空文字は許さない。
年齢は 0 以上で、増やし方は haveBirthday() に閉じ込める。
外からは、こうしかできません。
User u = new User(1, "Taro", 20);
u.haveBirthday();
u.rename("Jiro");
System.out.println(u.getAge());
Java「User らしさ」を守る責任が、
完全に User の中に戻ってきています。
これが、カプセル化された“まともなクラス”の姿です。
例題2:DTO 的なクラスと“本気のドメインクラス”
「ただの入れ物」と「ルールを持つ存在」
現場では、
「ただデータを運ぶだけのクラス(DTO)」と
「ルールを持つクラス(ドメイン)」が
混在します。
DTO 的なクラスは、こういうやつです。
public class UserDto {
public int id;
public String name;
public int age;
}
Javaこれは「API の入出力をそのまま表す」みたいな用途では
アリです。
ルールは別の層でチェックする前提だからです。
でも、
アプリの“中心”で使うクラスは、
DTO のままだと苦しくなります。
そこで、
「外とのやり取りは DTO、
アプリの中では“本気の User”」
という分け方がよく使われます。
public class UserFactory {
public User fromDto(UserDto dto) {
return new User(dto.id, dto.name, dto.age);
}
}
Javaこうすると、
外から変な値が来ても、
User のコンストラクタで弾けます。
カプセル化の視点で言うと、
「ルールを持つクラス」と
「ただ運ぶだけのクラス」を意識的に分ける
ということです。
重要ポイントの深掘り1:不変オブジェクトという究極の「直接触らせない」
「一度作ったら二度と変わらない」という設計
カプセル化の究極形の一つが、
不変オブジェクト(immutable object) です。
例えば、こんなクラス。
public class Money {
private final int amount;
public Money(int amount) {
if (amount < 0) {
throw new IllegalArgumentException("金額は 0 以上");
}
this.amount = amount;
}
public int getAmount() {
return amount;
}
public Money add(Money other) {
return new Money(this.amount + other.amount);
}
public Money subtract(Money other) {
if (other.amount > this.amount) {
throw new IllegalArgumentException("マイナスにはできない");
}
return new Money(this.amount - other.amount);
}
}
Javaここでは、amount は private final。setter は存在しない。add や subtract は「新しい Money を返す」。
つまり、
「一度作った Money の中身は二度と変わらない」
という設計です。
これは極端に「直接触らせない」形ですが、
その分メリットも大きいです。
どこからも書き換えられないので、
状態の追跡が楽で、バグが減ります。
全部を不変にする必要はありませんが、
「変わる必要のないものは不変にする」という選択肢を
持っておくと、設計の幅が広がります。
重要ポイントの深掘り2:「テストしやすさ」から見るカプセル化
ルールが散らばっているとテストも散らばる
カプセル化が効いているクラスは、
テストコードを書くときにも
「気持ちよさ」が違います。
例えば、ちゃんとカプセル化された BankAccount。
public class BankAccount {
private int balance;
public BankAccount(int initialBalance) {
if (initialBalance < 0) throw new IllegalArgumentException();
this.balance = initialBalance;
}
public int getBalance() {
return balance;
}
public void deposit(int amount) { ... }
public void withdraw(int amount) { ... }
}
Javaテストは、deposit と withdraw を呼んでgetBalance() を確認するだけで済みます。
「残高がマイナスにならないか」
「初期残高がマイナスのとき例外が出るか」
全部、このクラスのテストだけで完結します。
逆に、
残高チェックを外側のクラスでやっていたら、
テストもあちこちに散らばります。
「ルールがクラスの中に集まっているほど、
テストもそのクラスに集められる」。
これも、カプセル化の大きな効用です。
5日目の実践:自分のクラスを“診断”してみる
3つの質問で、カプセル化レベルをチェックする
今日やってみてほしいのは、
自分の書いたクラスに対して
次の3つを問いかけることです。
このクラスのフィールドは、全部 private になっているか?
なっていないなら、なぜ公開しているのか説明できるか?
このクラスには「壊れてはいけない約束(不変条件)」があるか?
あるなら、その約束を破る操作はクラスの外に出ていないか?
このクラスを使う側のコードは、get と set だけでゴリゴリ計算していないか?
それ、本当はこのクラスの「意味のあるメソッド」にできないか?
この3つに「はい、説明できる」と言えるクラスは、
もう立派に“カプセル化されたクラス」です。
5日目で本当に掴んでほしいこと
5日目のテーマは、
「カプセル化された“まともなクラス”の姿を
自分の中でハッキリ描けるようにする」 ことでした。
フィールドは private。
必要なものだけ、意図を持って getter / setter。
壊れてはいけない約束(不変条件)はクラスの中に閉じ込める。
外からは「意味のあるメソッド」だけが見える。
ここまで来たあなたは、
もう「カプセル化を知っている人」ではなく、
「カプセル化されたクラスを設計できる人」 です。
この感覚を持ったまま、
6日目・7日目では
カプセル化と継承・インターフェース・複数クラスの連携
といった、さらに一段上のオブジェクト指向に
自然に踏み込んでいけます。
