24.

OOP カプセル化の具体例完全ガイド — private/getter・不変・Builder・records

編集
この記事の要点
  • カプセル化 (Encapsulation) は OOP の 4 大原則の 1 つ。データを private で隠蔽し、操作を public メソッド経由に限定する
  • 基本パターン: private フィールド + public getter/setter。setter で不変条件 (invariant) をチェック
  • 上位パターン: 不変オブジェクト (Immutable) — setter を作らない。java.lang.String / LocalDate / BigDecimal が代表例
  • Builder Pattern でフィールド多いクラスでも不変性を保つ。Java records (Java 14+) は records だけで自動カプセル化
  • PHP 8.1 readonly プロパティで一度だけ代入可能。List.copyOf()防御的コピーで参照漏れ防止

カプセル化とは

カプセル化 (Encapsulation) とは、オブジェクトの内部データ (フィールド) を外部から直接触れないように隠し、決められた手続き (メソッド) を通してのみアクセスを許す設計原則です。継承・ポリモーフィズム・抽象化と並ぶ OOP の 4 大原則の 1 つに数えられます。

カプセル化を守ることで:

  • 不変条件 (invariant) を保てる — 残高がマイナスにならない、年齢が 0〜150 の範囲、など
  • 内部実装を変更しても外部 API に影響しない — フィールド名変更・データ構造変更が自由に
  • 並行アクセスや競合の制御点を 1 つに集約できる
  • テスト・ログ追加・バリデーション差し込みが容易

具体例1: 銀行口座 (Bank Account) — 古典的な例

最も典型的なのは銀行口座クラスです。残高 balancepublic にすると、外部から自由に書き換えられてしまい、マイナス残高や予期しない値になり得ます。

// 悪い例: カプセル化されていない
public class BadAccount {
    public long balance;          // 誰でも触れる
}

BadAccount a = new BadAccount();
a.balance = -1_000_000;            // マイナス残高が許される
a.balance = Long.MAX_VALUE;        // オーバーフロー間近

// 良い例: カプセル化された Account
public class Account {
    private long balance;          // 隠蔽
    private final String accountNo;

    public Account(String accountNo, long initial) {
        if (initial < 0) throw new IllegalArgumentException("初期残高は 0 以上");
        this.accountNo = accountNo;
        this.balance = initial;
    }

    public long getBalance() {
        return balance;
    }

    public void deposit(long amount) {
        if (amount <= 0) throw new IllegalArgumentException("入金額は正の数");
        balance += amount;
    }

    public void withdraw(long amount) {
        if (amount <= 0) throw new IllegalArgumentException("出金額は正の数");
        if (balance < amount) throw new IllegalStateException("残高不足: " + balance);
        balance -= amount;
    }
}

Account a = new Account("001", 1000);
a.deposit(500);                    // OK
a.withdraw(200);                   // OK
// a.balance = -1; ← コンパイルエラー: private

残高へのあらゆる変更が deposit / withdraw を通るため、不変条件 (残高 >= 0) を常に保てます。

具体例2: 不変オブジェクト (Immutable)

setter を一切作らず、生成時に決めた値を変えられないオブジェクトを不変オブジェクトと呼びます。スレッドセーフかつバグの混入余地が少なく、現代の OOP では強く推奨されます。

import java.time.LocalDate;
import java.util.Objects;

public final class Person {                 // final で継承禁止
    private final String name;              // final で再代入禁止
    private final LocalDate birthDate;

    public Person(String name, LocalDate birthDate) {
        this.name = Objects.requireNonNull(name);
        this.birthDate = Objects.requireNonNull(birthDate);
    }

    public String getName() { return name; }
    public LocalDate getBirthDate() { return birthDate; }

    // 名前変更は「新しいインスタンスを返す」
    public Person withName(String newName) {
        return new Person(newName, this.birthDate);
    }
}

Person p1 = new Person("太郎", LocalDate.of(1990, 1, 1));
Person p2 = p1.withName("次郎");           // p1 はそのまま、p2 は新しいインスタンス

標準ライブラリの不変クラス例:

クラス特徴
java.lang.String文字列。連結や置換は新しい String を返す
java.time.LocalDate / LocalDateTime / Instant日時の不変クラス (Java 8+)
java.math.BigDecimal / BigInteger任意精度の不変数値
Integer / Long / Double 等のラッパープリミティブのボックス化版
List.of() / Map.of() / Set.of() (Java 9+)変更不可コレクション

具体例3: Java records (Java 14+) — 自動カプセル化

records は不変なデータキャリアを 1 行で宣言できる構文。等価性 (equals) / ハッシュ / 文字列化 / アクセサ / コンストラクタが自動生成されます。

// Java 14+
public record Money(long amount, String currency) {
    // コンパクトコンストラクタで検証
    public Money {
        if (amount < 0) throw new IllegalArgumentException("負の金額不可");
        Objects.requireNonNull(currency);
    }
}

Money m = new Money(1000, "JPY");
System.out.println(m.amount());           // → 1000  (getAmount ではなくフィールド名)
System.out.println(m.currency());         // → JPY
System.out.println(m);                    // → Money[amount=1000, currency=JPY]
// m.amount = 2000; ← コンパイルエラー: record は不変

具体例4: Builder Pattern

フィールドが多いと、不変オブジェクトでもコンストラクタ引数が長大になります。Builder Pattern で読みやすさと不変性を両立:

public final class HttpRequest {
    private final String url;
    private final String method;
    private final Map<String, String> headers;
    private final String body;

    private HttpRequest(Builder b) {
        this.url = b.url;
        this.method = b.method;
        this.headers = Map.copyOf(b.headers);  // 防御的コピー
        this.body = b.body;
    }

    public static Builder builder() { return new Builder(); }

    public static class Builder {
        private String url;
        private String method = "GET";
        private Map<String, String> headers = new HashMap<>();
        private String body = "";

        public Builder url(String u) { this.url = u; return this; }
        public Builder method(String m) { this.method = m; return this; }
        public Builder header(String k, String v) { this.headers.put(k, v); return this; }
        public Builder body(String b) { this.body = b; return this; }
        public HttpRequest build() {
            if (url == null) throw new IllegalStateException("url 必須");
            return new HttpRequest(this);
        }
    }
}

HttpRequest req = HttpRequest.builder()
    .url("https://example.com")
    .method("POST")
    .header("Content-Type", "application/json")
    .body("{}")
    .build();

具体例5: PHP 8.1 readonly

PHP 8.1 では readonly 修飾子で「初期化後は変更不可」のプロパティを宣言できます (Java の final 相当)。Java の records に近い感覚で不変クラスを書けます。

class Point {
    public function __construct(
        public readonly float $x,
        public readonly float $y,
    ) {}

    public function translate(float $dx, float $dy): Point {
        return new Point($this->x + $dx, $this->y + $dy);
    }
}

$p = new Point(1.0, 2.0);
echo $p->x;             // 1
// $p->x = 5;           // Error: Cannot modify readonly property

// PHP 8.2 では「クラス丸ごと readonly」も可能
final readonly class Money {
    public function __construct(
        public int $amount,
        public string $currency,
    ) {}
}

具体例6: 防御的コピー (Defensive Copy)

不変クラスで List / Map など可変コレクションを保持する場合、受け取った参照をそのまま持つと外から書き換えられて不変性が崩れます。

public final class Team {
    private final List<String> members;

    // ❌ 危険: 参照を共有
    public Team(List<String> members) {
        this.members = members;        // 外から add される可能性
    }

    // ✅ 防御的コピー
    public Team(List<String> members) {
        this.members = List.copyOf(members);   // Java 10+ : 不変コピー
    }

    public List<String> getMembers() {
        // ✅ そのまま返しても List.copyOf() なので変更不可
        return members;
    }
}

List<String> src = new ArrayList<>(List.of("Alice", "Bob"));
Team t = new Team(src);
src.add("Charlie");                     // ✅ t には影響しない
t.getMembers().add("Dave");             // UnsupportedOperationException

カプセル化のレベル比較

レベル用途
0 (無し)public int x;非推奨。テスト用構造体程度
1 (基本)private + getter/setterJavaBeans、Hibernate エンティティ
2 (検証付き)setter で不変条件チェック業務エンティティ
3 (不変)setter 無し、final / readonly値オブジェクト (Value Object)
4 (Builder)不変 + Builder で組立フィールド多数の Config 系
5 (record / readonly class)言語機能で完結DTO / レスポンス型

FAQ

Q: getter/setter を全フィールドに付けるのは「カプセル化」と言える?
A: 形だけで実質はノーガード。setter で不変条件を検証するか、思い切って不変クラスにするのが本来のカプセル化です。

Q: Lombok の @Data は使うべき?
A: 手早く JavaBeans を作るには便利ですが、setter が全公開されるので業務エンティティには不向き@Value (不変) や records を優先しましょう。

Q: 不変オブジェクトはメモリを食わない?
A: 大量に新インスタンスを作るとオーバーヘッドがありますが、JVM の世代別 GC は短命オブジェクトに強いため、ほとんどのケースで性能問題にはなりません。むしろスレッドセーフ性とバグ削減のメリットが圧倒的に大きいです。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. 基本事項
  2. HTMLへの埋め込み
  3. 変数
  4. 可変変数
  5. 定数
  6. データ型
  7. キャスト
  8. エスケープ文字
  9. 配列
  10. 演算子
  11. 代入の際の注意点
  12. 条件分岐
  13. 繰り返し処理
  14. クラスとインスタンス
  15. コンストラクタ
  16. 関数
  17. スーパーグローバル変数
  18. スコープ
  19. staticについて
  20. yieldについて
  21. ファイルのアップロード方法
  22. DB接続方法
  23. SQL実行方法
  24. カプセル化の具体例
  25. 継承の構文
  26. オーバーライド
  27. ポリモーフィズム(多様性)の具体例
  28. 抽象クラス・メソッドの構文と具体例
  29. GET通信
  30. try catchで全てのエラーを拾う方法

最近更新/作成されたページ