この内容は古いバージョンです。最新バージョンを表示するには、戻るボタンを押してください。
バージョン:5
ページ更新者:T
更新日時:2026-06-11 07:07:02

タイトル: カプセル化
SEOタイトル: 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 にも読み書きできることが多い。アノテーション/属性次第。