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

Web APP Java
スポンサーリンク

3日目のゴール

3日目のテーマも
「直接触らせない」=カプセル化

1日目:privategetter / setter の基本と「中身を守る」感覚。
2日目:「何でも getter / setter」から卒業して、「何を見せるか」を設計する感覚。

3日目は、もう一歩踏み込んで
「クラスの中に“壊れてはいけない約束(不変条件)”を閉じ込める」
という視点で、privategetter / setter を見直します。


カプセル化の本質は「不変条件を守ること」

不変条件って何?

不変条件(invariant)というのは、
「このクラスのインスタンスは、常にこういう状態であるべき」
という約束のことです。

例えば、PointCard なら

ポイントは必ず 0 以上
マイナスにはならない

BankAccount なら

残高は必ず 0 以上
初期残高も 0 以上

User なら

年齢は 0 以上
ID は一度決まったら変わらない

こういう「壊れてはいけない約束」を
クラスの外に任せるのではなく、
クラスの中で守る
のがカプセル化の本当の役割です。

private は「外から直接いじれないようにする」ための鍵。
getter / setter は「約束を守らせるための窓口」。

3日目は、この「約束を守る」という視点で
コードを見直していきます。


例題1:PointCard に“不変条件”の意識を入れる

「0以上」という約束をクラスの中に閉じ込める

改めて、PointCard を書いてみます。

public class PointCard {
    private int point;

    public PointCard() {
        this.point = 0;
    }

    public int getPoint() {
        return point;
    }

    public void addPoint(int amount) {
        if (amount < 0) {
            throw new IllegalArgumentException("追加ポイントは0以上");
        }
        this.point += amount;
    }

    public void usePoint(int amount) {
        if (amount < 0) {
            throw new IllegalArgumentException("使用ポイントは0以上");
        }
        if (amount > point) {
            throw new IllegalArgumentException("ポイントが足りません");
        }
        this.point -= amount;
    }
}
Java

ここでの“不変条件”は
「point は常に 0 以上」 です。

重要なのは、

コンストラクタで 0 に初期化している
addPoint でマイナスを拒否している
usePoint でマイナスと残高超えを拒否している
point を直接書き換える手段(setter)が存在しない

ということです。

つまり、
「どんなメソッドをどう呼んでも、
このクラスの中では point がマイナスにならない」

という状態を作っている。

これが「不変条件をクラスの中に閉じ込める」ということです。


例題2:BankAccount の“不変条件”を言葉にしてからコードにする

先に「約束」を日本語で書いてみる

いきなりコードを書く前に、
あえて日本語でこう書いてみます。

このクラスの約束:

残高は必ず 0 以上
初期残高も 0 以上
入金額は 1 以上
出金額も 1 以上
残高以上は引き出せない

これを守るように、
private とメソッドを設計します。

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

ここでも同じです。

balanceprivate
setBalance は存在しない
depositwithdraw の中でルールを守らせている

だから、
「どんな呼び方をしても、balance がマイナスになることはない」
という不変条件が守られます。


重要ポイントの深掘り1:setter は“不変条件の門番”

「ただ代入するだけの setter」は危険

よくある setter はこうです。

public void setAge(int age) {
    this.age = age;
}
Java

これは
「門番がいない門」 です。

誰でも、どんな値でも通せてしまう。

3日目の視点では、
setter はこうあるべきです。

public void setAge(int age) {
    if (age < 0) {
        throw new IllegalArgumentException("年齢は0以上");
    }
    this.age = age;
}
Java

つまり、
「不変条件を破る値は門前払いする」 のが setter の役割。

もし不変条件がもっと複雑なら、
そのロジックも setter の中に書きます。

「18歳未満は登録できない」
「名前は空文字はダメ」
「メールアドレスの形式をチェックする」

こういうルールを
クラスの外に任せず、クラスの中に閉じ込める

これがカプセル化の“設計としての使い方”です。


重要ポイントの深掘り2:「読み取り専用」と「書き込み可」を分ける

すべてに setter が必要なわけじゃない

2日目でも触れましたが、
3日目ではもう少し踏み込みます。

例えば、ユーザーの ID。

public class User {
    private final int id;
    private String name;

    public User(int id, String name) {
        this.id   = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}
Java

ここでの不変条件は、

ID は一度決まったら変わらない
名前は変わってもいい

だから、

idfinal + getter のみ
name は getter + setter

という設計になっています。

「読み取り専用にしたいものは getter だけ」
「変わってもいいものだけ setter を用意する」

この区別ができるようになると、
クラスの“壊れにくさ”が一気に上がります。


重要ポイントの深掘り3:コレクションを返す getter の罠

「中身を丸ごと渡す」とカプセル化が破れる

少しレベルを上げて、
リストを持つクラスを考えてみます。

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

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

    public void addItem(String item) {
        items.add(item);
    }
}
Java

一見、普通の getter に見えますが、
ここに落とし穴があります。

外からこう書けてしまいます。

TodoList list = new TodoList();
list.addItem("買い物");
List<String> ref = list.getItems();
ref.clear();  // 中身を全部消せる
Java

getItems()
「内部のリストそのもの」 を返しているので、
外から直接いじれてしまう。

これは、
private にしている意味が薄くなってしまうパターンです。

カプセル化を守るなら、
こういう書き方がよく使われます。

public List<String> getItems() {
    return new ArrayList<>(items); // コピーを返す
}
Java

こうすると、
外側で clear() しても、
内部の items には影響しません。

あるいは、
「読み取り専用ビュー」を返す方法もあります。

public List<String> getItems() {
    return Collections.unmodifiableList(items);
}
Java

これだと、外から addclear を呼ぶと
例外が出ます。

ここでのポイントは、

「getter だからといって、
内部の実体をそのまま渡していいとは限らない」

ということです。

カプセル化を守るために、
「どう返すか」も設計の一部 になります。


3日目の実践:自分のクラスに“不変条件ラベル”を貼る

コードを書く前に、日本語で約束を書いてみる

今日やってほしい練習は、とてもシンプルです。

クラスごとに、
「このクラスのインスタンスは、常にこういう状態であるべき」
という一文を日本語で書いてみる。

例えば PointCard なら、

このクラスの point は、常に 0 以上である。

BankAccount なら、

このクラスの balance は、常に 0 以上である。

User なら、

このクラスの id は、一度決まったら変わらない。

それを書いてから、
コンストラクタ・getter・setter・その他のメソッドを
「その約束を絶対に破らないように」 設計してみてください。

もし、

このメソッドを通すと約束が破れる
この setter があると不変条件が守れない

と気づいたら、
それは 「そのメソッドは公開すべきじゃない」
というサインです。


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

3日目のカプセル化のテーマは、
「直接触らせない」から
「約束を破らせない」に一段引き上げること
でした。

private は「鍵」。
getter / setter は「窓口」。

でも本当に守りたいのは、
「このクラスは、常にこういう状態でいてほしい」
という不変条件です。

そのために、

何を private にするか
どのフィールドに setter を付けるか
setter の中で何をチェックするか
コレクションをどう返すか

を、意識して選べるようになる。

ここまで来ると、
あなたのクラスはもう
「ただのデータの入れ物」ではなく、
「自分のルールを自分で守る小さな世界」

になっています。

この感覚を持ったまま、
4日目以降は
「カプセル化 × 役割分担(OOP)」の組み合わせに
踏み込んでいけます。

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