3日目のゴール
3日目のテーマも
「直接触らせない」=カプセル化。
1日目:private と getter / setter の基本と「中身を守る」感覚。
2日目:「何でも getter / setter」から卒業して、「何を見せるか」を設計する感覚。
3日目は、もう一歩踏み込んで
「クラスの中に“壊れてはいけない約束(不変条件)”を閉じ込める」
という視点で、private と getter / 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ここでも同じです。
balance は privatesetBalance は存在しないdeposit と withdraw の中でルールを守らせている
だから、
「どんな呼び方をしても、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 は一度決まったら変わらない
名前は変わってもいい
だから、
id は final + 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(); // 中身を全部消せる
JavagetItems() が
「内部のリストそのもの」 を返しているので、
外から直接いじれてしまう。
これは、private にしている意味が薄くなってしまうパターンです。
カプセル化を守るなら、
こういう書き方がよく使われます。
public List<String> getItems() {
return new ArrayList<>(items); // コピーを返す
}
Javaこうすると、
外側で clear() しても、
内部の items には影響しません。
あるいは、
「読み取り専用ビュー」を返す方法もあります。
public List<String> getItems() {
return Collections.unmodifiableList(items);
}
Javaこれだと、外から add や clear を呼ぶと
例外が出ます。
ここでのポイントは、
「getter だからといって、
内部の実体をそのまま渡していいとは限らない」
ということです。
カプセル化を守るために、
「どう返すか」も設計の一部 になります。
3日目の実践:自分のクラスに“不変条件ラベル”を貼る
コードを書く前に、日本語で約束を書いてみる
今日やってほしい練習は、とてもシンプルです。
クラスごとに、
「このクラスのインスタンスは、常にこういう状態であるべき」
という一文を日本語で書いてみる。
例えば PointCard なら、
このクラスの point は、常に 0 以上である。
BankAccount なら、
このクラスの balance は、常に 0 以上である。
User なら、
このクラスの id は、一度決まったら変わらない。
それを書いてから、
コンストラクタ・getter・setter・その他のメソッドを
「その約束を絶対に破らないように」 設計してみてください。
もし、
このメソッドを通すと約束が破れる
この setter があると不変条件が守れない
と気づいたら、
それは 「そのメソッドは公開すべきじゃない」
というサインです。
3日目で本当に掴んでほしいこと
3日目のカプセル化のテーマは、
「直接触らせない」から
「約束を破らせない」に一段引き上げること でした。
private は「鍵」。getter / setter は「窓口」。
でも本当に守りたいのは、
「このクラスは、常にこういう状態でいてほしい」
という不変条件です。
そのために、
何を private にするか
どのフィールドに setter を付けるか
setter の中で何をチェックするか
コレクションをどう返すか
を、意識して選べるようになる。
ここまで来ると、
あなたのクラスはもう
「ただのデータの入れ物」ではなく、
「自分のルールを自分で守る小さな世界」
になっています。
この感覚を持ったまま、
4日目以降は
「カプセル化 × 役割分担(OOP)」の組み合わせに
踏み込んでいけます。
