25.

OOP のカプセル化(Encapsulation)— なぜ必要か・現代言語での書き方

編集
この記事の要点
  • カプセル化: オブジェクト内部のデータと振る舞いをまとめ、外部から直接アクセスさせないこと
  • アクセス修飾子: private / protected / public (+ Java の package-private, C# の internal)
  • 目的: 変更耐性(内部実装を隠す)、不整合防止(不正な値を防ぐ)、テスト容易性
  • getter/setter は必ず付ける必要は無い。本当に必要な操作だけを公開メソッドにする
  • 現代的書き方: Java の record (14+) / C# の record / Kotlin の data class / PHP 8.1+ の readonly プロパティ

カプセル化とは

カプセル化(Encapsulation)はオブジェクト指向プログラミングの 3 大特徴のひとつで(残り 2 つは継承とポリモーフィズム)、次の 2 つの側面を持ちます:

  1. データと操作の束ね: 関連するデータ(フィールド)とそれを扱うメソッドを 1 つのクラスにまとめる
  2. 情報隠蔽 (Information Hiding): 内部状態を外部から直接触らせず、決められたメソッド経由でのみ操作させる

後者の情報隠蔽が特に重要で、これが守られていればクラス内部の実装をいくら変えても、外部のコードは壊れません

アクセス修飾子

修飾子同クラスサブクラス同パッケージ外部
private×××
protected○ (Java)×
(default/package)×○ (Java)×
public

カプセル化していない例(悪い)

// ❌ 全フィールドが public
public class BankAccount {
    public String owner;
    public double balance;
}

// 外部から自由に書き換え可能
BankAccount a = new BankAccount();
a.balance = -1_000_000;   // マイナス残高でも入る
a.balance += 100;          // どこからでも増減可

この設計だと「残高はマイナスにならない」「2 重に引き落とせない」といったビジネスルールを守るのが不可能です。

カプセル化した例(良い)

public class BankAccount {
    private final String owner;
    private double balance;

    public BankAccount(String owner, double initial) {
        if (initial < 0) throw new IllegalArgumentException("initial >= 0");
        this.owner = owner;
        this.balance = initial;
    }

    public String getOwner() { return owner; }

    public double getBalance() { return balance; }

    public void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("amount > 0");
        balance += amount;
    }

    public void withdraw(double amount) {
        if (amount <= 0)         throw new IllegalArgumentException("amount > 0");
        if (amount > balance)    throw new IllegalStateException("残高不足");
        balance -= amount;
    }
}

// 利用側
BankAccount a = new BankAccount("Alice", 1000);
a.deposit(500);
a.withdraw(300);
// a.balance = -1; ← コンパイルエラー(private)
  • balance は private なので外から直接書き換えられない
  • deposit / withdraw 経由でしか変更できず、不正な値は弾かれる
  • ownerfinal + private で読み取り専用(不変)

PHP での書き方

class BankAccount
{
    public function __construct(
        private readonly string $owner,   // PHP 8.1+: readonly
        private float $balance = 0.0
    ) {
        if ($balance < 0) throw new InvalidArgumentException();
    }

    public function getOwner(): string  { return $this->owner; }
    public function getBalance(): float { return $this->balance; }

    public function deposit(float $amount): void
    {
        if ($amount <= 0) throw new InvalidArgumentException();
        $this->balance += $amount;
    }

    public function withdraw(float $amount): void
    {
        if ($amount > $this->balance) throw new RuntimeException("残高不足");
        $this->balance -= $amount;
    }
}

Python での書き方

Python には「言語レベルの private」はありませんが、慣習で _ プレフィックス + @property で実現します:

class BankAccount:
    def __init__(self, owner: str, initial: float = 0):
        self._owner = owner
        self._balance = initial

    @property
    def owner(self) -> str:
        return self._owner   # 読み取り専用

    @property
    def balance(self) -> float:
        return self._balance

    def deposit(self, amount: float) -> None:
        if amount <= 0: raise ValueError
        self._balance += amount

    def withdraw(self, amount: float) -> None:
        if amount > self._balance: raise RuntimeError("残高不足")
        self._balance -= amount


a = BankAccount("Alice", 1000)
print(a.balance)   # @property のおかげでメソッド呼び出しに見えない
# a.balance = -1  # setter を定義していないので AttributeError

不変オブジェクト (Immutable Object)

カプセル化の極致が不変オブジェクト。一度作ったら状態が変えられないので、マルチスレッド安全かつバグの温床がゼロになります:

// Java の record(14+)
public record Point(int x, int y) {
    public Point {
        if (x < 0 || y < 0) throw new IllegalArgumentException();
    }
}

// 利用
Point p = new Point(10, 20);
int x = p.x();         // getter 自動生成
// p.x = 30; ← コンパイルエラー(final)

現代言語の「データクラス」

言語機能
Java 14+recordrecord Point(int x, int y) {}
C# 9+record / initrecord Point(int X, int Y);
Kotlindata classdata class Point(val x:Int, val y:Int)
PHP 8.1+readonlypublic readonly int $x
Python 3.7+dataclass@dataclass(frozen=True)
TypeScriptreadonlyreadonly x: number

カプセル化と getter/setter のアンチパターン

「全フィールドに無条件で getter/setter を生やす」のはカプセル化していないのと同じです。意味のない getter/setter は責務分割の機会を失うだけ:

// ❌ getter/setter を機械的に生やす(カプセル化の意味なし)
public class User {
    private String name;
    public String getName() { return name; }
    public void setName(String n) { this.name = n; }
}

// ✅ 必要な操作だけ公開する
public class User {
    private String name;
    public String displayName() {
        return name.isEmpty() ? "(no name)" : name;
    }
    public void rename(String newName) {
        if (newName.isBlank()) throw new IllegalArgumentException();
        this.name = newName;
    }
}

カプセル化のメリットまとめ

  • 変更耐性: 内部実装の変更が外部に伝播しない
  • 不整合防止: 不正な状態に絶対ならない
  • テスト容易性: 公開 API だけテストすればよい
  • 並行処理安全性: 状態の出入り口を絞れる(不変なら最強)
  • ドキュメント代わり: public な API がそのまま使い方

FAQ

Q: private と final の違い
A: private は可視性(外から見えるか)、final は変更可能性(再代入できるか)。両方つけると「外から見えず再代入もできない」最強の隠蔽。

Q: getter は遅い?
A: JIT が即座にインライン化するので、フィールド直接アクセスと性能差はほぼ無い。

Q: フレームワーク(JPA/Doctrine 等)が public を要求する
A: ORM はリフレクションで private にも読み書きできることが多い。アノテーション/属性次第。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. 基本的なルール
  2. データ型
  3. 変数
  4. 定数
  5. 配列
  6. コレクション(List,Set,Queue)
  7. Map(連想配列)
  8. 演算子
  9. 条件分岐
  10. 繰り返し制御文
  11. クラス
  12. メソッド
  13. インスタンス化
  14. コンストラクタ
  15. staticキーワード
  16. オーバーロード
  17. 継承
  18. オーバーライド
  19. this
  20. super
  21. パッケージ
  22. アクセス修飾子
  23. 抽象クラス・メソッド
  24. インターフェース
  25. カプセル化
  26. データベース接続
  27. セッション
  28. ファイル入出力
  29. ラムダ式