Java | 1 日 120 分 × 7 日アプリ学習 中級編:オブジェクト指向(OOP) - カプセル化アプリ

Web APP Java
スポンサーリンク

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

ここでは、
amountprivate final
setter は存在しない。
addsubtract は「新しい 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

テストは、
depositwithdraw を呼んで
getBalance() を確認するだけで済みます。

「残高がマイナスにならないか」
「初期残高がマイナスのとき例外が出るか」

全部、このクラスのテストだけで完結します。

逆に、
残高チェックを外側のクラスでやっていたら、
テストもあちこちに散らばります。

「ルールがクラスの中に集まっているほど、
テストもそのクラスに集められる」

これも、カプセル化の大きな効用です。


5日目の実践:自分のクラスを“診断”してみる

3つの質問で、カプセル化レベルをチェックする

今日やってみてほしいのは、
自分の書いたクラスに対して
次の3つを問いかけることです。

このクラスのフィールドは、全部 private になっているか?
なっていないなら、なぜ公開しているのか説明できるか?

このクラスには「壊れてはいけない約束(不変条件)」があるか?
あるなら、その約束を破る操作はクラスの外に出ていないか?

このクラスを使う側のコードは、
getset だけでゴリゴリ計算していないか?
それ、本当はこのクラスの「意味のあるメソッド」にできないか?

この3つに「はい、説明できる」と言えるクラスは、
もう立派に“カプセル化されたクラス」です。


5日目で本当に掴んでほしいこと

5日目のテーマは、
「カプセル化された“まともなクラス”の姿を
自分の中でハッキリ描けるようにする」
ことでした。

フィールドは private
必要なものだけ、意図を持って getter / setter
壊れてはいけない約束(不変条件)はクラスの中に閉じ込める。
外からは「意味のあるメソッド」だけが見える。

ここまで来たあなたは、
もう「カプセル化を知っている人」ではなく、
「カプセル化されたクラスを設計できる人」 です。

この感覚を持ったまま、
6日目・7日目では
カプセル化と継承・インターフェース・複数クラスの連携
といった、さらに一段上のオブジェクト指向に
自然に踏み込んでいけます。

タイトルとURLをコピーしました