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

Web APP Java
スポンサーリンク

6日目のゴール

6日目のテーマも「直接触らせない」=カプセル化。
ただ、今日はもう一段視点を広げて
「クラス同士がつながったときに、どこまで触らせるか」
を考える日です。

キーワードは変わらず privategetter / setter
でも、焦点は
「アクセス範囲をどう設計するか」
に移ります。


カプセル化は「1クラスの中」だけの話では終わらない

クラス同士がつながるとき、情報はどこまで流すか

ここまでで、1つのクラスの中では

フィールドは private
不変条件を中に閉じ込める
意味のあるメソッドだけを外に見せる

というところまで来ました。

6日目では、
「クラス同士がつながったときに、
どこまで情報を渡すか」

を意識します。

例えば、

User と UserService
PointCard と PaymentService
BankAccount と TransferService

こういう「ペア」で考えたとき、
「どこまで中身を見せるか」
カプセル化の勝負どころになります。


例題1:BankAccount と TransferService

素直だけど“触りすぎている”コード

まず、銀行口座クラス。

public class BankAccount {
    private int balance;

    public BankAccount(int initialBalance) {
        if (initialBalance < 0) {
            throw new IllegalArgumentException("初期残高は0以上");
        }
        this.balance = initialBalance;
    }

    public int getBalance() {
        return balance;
    }

    public void deposit(int amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("入金額は1以上");
        }
        this.balance += amount;
    }

    public void withdraw(int amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("出金額は1以上");
        }
        if (amount > balance) {
            throw new IllegalArgumentException("残高不足");
        }
        this.balance -= amount;
    }
}
Java

ここまではいい感じです。
では、振込サービスを考えます。

public class TransferService {

    public void transfer(BankAccount from, BankAccount to, int amount) {
        int fromBalance = from.getBalance();
        if (amount <= 0) {
            throw new IllegalArgumentException("振込額は1以上");
        }
        if (amount > fromBalance) {
            throw new IllegalArgumentException("残高不足");
        }

        from.withdraw(amount);
        to.deposit(amount);
    }
}
Java

一見、正しいように見えます。
でも、ここで立ち止まってみると、

TransferService が
「残高を直接見て、振込可能かどうかを判断している」

つまり、
「残高の事情を知りすぎている」 状態です。


例題1の改善:BankAccount に「振込可能か」を聞く

「残高のルール」は口座側に閉じ込める

残高のルールは、
本来 BankAccount が知っているべきです。

そこで、BankAccount に
「この金額を引き出せるか?」を聞くメソッドを足します。

public class BankAccount {
    private int balance;

    public BankAccount(int initialBalance) {
        if (initialBalance < 0) {
            throw new IllegalArgumentException("初期残高は0以上");
        }
        this.balance = initialBalance;
    }

    public int getBalance() {
        return balance;
    }

    public boolean canWithdraw(int amount) {
        if (amount <= 0) {
            return false;
        }
        return amount <= balance;
    }

    public void withdraw(int amount) {
        if (!canWithdraw(amount)) {
            throw new IllegalArgumentException("出金できません");
        }
        this.balance -= amount;
    }

    public void deposit(int amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("入金額は1以上");
        }
        this.balance += amount;
    }
}
Java

TransferService は、こう書き換えられます。

public class TransferService {

    public void transfer(BankAccount from, BankAccount to, int amount) {
        if (!from.canWithdraw(amount)) {
            throw new IllegalArgumentException("振込元口座から引き出せません");
        }
        from.withdraw(amount);
        to.deposit(amount);
    }
}
Java

ここでの変化は、

残高のルールは BankAccount の中に閉じ込められた
TransferService は「振込できるか?」だけを聞けばよくなった

つまり、
「TransferService は、BankAccount の中身を直接触らず、
意味だけを呼び出す」
形になったことです。

これが、
クラス同士の関係における「直接触らせない」です。


重要ポイントの深掘り1:「Law of Demeter(デメテルの法則)」の感覚

「友達の友達の中身まで触りに行かない」

オブジェクト指向の世界には
「デメテルの法則」という考え方があります。

ざっくり言うと、

「自分の直接の相手だけと話しなさい。
相手の中身の中身まで、手を突っ込まない。」

という感覚です。

例えば、こういうコード。

user.getAddress().getCity().setName("Tokyo");
Java

これは、

User の中の Address の中の City の中の name

まで、手を突っ込んでいる状態です。

カプセル化の視点から見ると、
「階層をまたいで中身を触りすぎている」 ということになります。

6日目で意識してほしいのは、

obj.getX().getY().getZ()

みたいなコードを見たら、
「それ、本当は obj にメソッドを足すべきじゃない?」
と疑ってみることです。

例えば、

user.changeCityName("Tokyo");
Java

のように、
User に「意味のあるメソッド」を足して、
中身の中身の事情を外に漏らさない。

これが、
「直接触らせない」を
クラス間の関係に広げた形です。


例題2:TodoList と UI クラス

UI が「中身のリスト」を触りすぎているパターン

TodoList クラスを考えます。

public class TodoList {
    private final List<String> items = new ArrayList<>();

    public void add(String item) {
        if (item == null || item.isBlank()) {
            throw new IllegalArgumentException("空のタスクは追加できません");
        }
        items.add(item);
    }

    public void removeAt(int index) {
        if (index < 0 || index >= items.size()) {
            throw new IllegalArgumentException("範囲外です");
        }
        items.remove(index);
    }

    public List<String> getItems() {
        return new ArrayList<>(items);
    }
}
Java

UI 側がこう書いていたとします。

public class TodoUi {

    private final TodoList list;

    public TodoUi(TodoList list) {
        this.list = list;
    }

    public void clearIfTooMany() {
        List<String> items = list.getItems();
        if (items.size() > 100) {
            items.clear();  // これは内部には効かないが、やりたいことは「全部消す」
        }
    }
}
Java

ここでやりたいのは
「タスクが多すぎたら全部消したい」。

でも、
UI が「リストの中身」を直接いじろうとしている。

本当にやりたいことは、
「TodoList に『全部消して』と頼みたい」 はずです。


例題2の改善:TodoList に「全部消す」メソッドを足す

「操作の意味」をクラス側に集める

TodoList に、こういうメソッドを足します。

public class TodoList {
    private final List<String> items = new ArrayList<>();

    public void add(String item) { ... }

    public void removeAt(int index) { ... }

    public void clear() {
        items.clear();
    }

    public List<String> getItems() {
        return new ArrayList<>(items);
    }
}
Java

UI 側は、こう書けます。

public class TodoUi {

    private final TodoList list;

    public TodoUi(TodoList list) {
        this.list = list;
    }

    public void clearIfTooMany() {
        if (list.getItems().size() > 100) {
            list.clear();
        }
    }
}
Java

さらに一歩進めるなら、
「多すぎるかどうか」の判断も TodoList に寄せられます。

public class TodoList {
    private final List<String> items = new ArrayList<>();

    public boolean isTooMany(int limit) {
        return items.size() > limit;
    }

    public void clear() {
        items.clear();
    }
}
Java

UI はこう。

public void clearIfTooMany() {
    if (list.isTooMany(100)) {
        list.clear();
    }
}
Java

ここでのポイントは、

UI は「多すぎるかどうか」と「消す」という意味だけを扱う
リストのサイズや中身の事情は TodoList に閉じ込める

つまり、
「直接触らせない」ことで、
クラスごとの責務がハッキリ分かれる
ということです。


重要ポイントの深掘り2:アクセス修飾子を“設計の道具”として見る

private / package-private / protected / public の使い分け

ここまで privatepublic に集中してきましたが、
6日目では少しだけ視野を広げます。

Java には、アクセス修飾子が4種類あります。

private
クラスの中からだけ見える。

(何も付けない)
同じパッケージの中から見える。

protected
同じパッケージ+サブクラスから見える。

public
どこからでも見える。

カプセル化の視点で言うと、

「本当に外から見えてほしいものだけ public」
「同じパッケージの中だけで使いたいものは package-private」
「クラスの中だけで完結させたいものは private」

というふうに、
「見える範囲を意図的に絞る」 のが設計です。

例えば、
ドメイン層の中だけで使う補助クラスは
public にする必要はありません。

class AgeCalculator {
    int calculate(LocalDate birthDate, LocalDate now) {
        return Period.between(birthDate, now).getYears();
    }
}
Java

こうしておけば、
他のパッケージからは見えません。

「直接触らせない」は、
「パッケージの外からも触らせない」
というレベルまで広げられます。


6日目の実践:クラス間の“触り方”を見直す

自分のコードに、こう問いかけてみる

今日やってほしいのは、
クラス同士の関係に対して
次の問いを投げることです。

このクラスは、相手の「中身」を触りすぎていないか?
getX().getY().getZ() みたいなコードになっていないか?

その処理、本当は相手のクラスに
「意味のあるメソッド」として置くべきじゃないか?

このメソッドは、本当に public である必要があるか?
同じパッケージの中だけでよければ、修飾子を落とせないか?

こうやって、
「どこまで触らせるか」を意識的に決める ことが
6日目のテーマです。


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

今日の「直接触らせない」は、
「フィールドに直接触らせない」から
「他のクラスの中身にも直接触らせない」へ

一段広がりました。

BankAccount の残高のルールは BankAccount に閉じ込める。
TodoList の中身の事情は TodoList に閉じ込める。
外側のクラスは「意味のあるメソッド」だけを呼ぶ。
アクセス修飾子で「見える範囲」を意図的に絞る。

ここまで来たあなたは、
もう「カプセル化をクラス単体で使える人」から
「カプセル化をクラス同士の関係にまで広げて設計できる人」
になっています。

7日目では、
この感覚を言葉にして
「自分のカプセル化ポリシー」として
まとめていくところまで行けます。

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