6.

OOP カプセル化の本質(データ隠蔽 / private / readonly / immutable / 言語別書き方)

編集
この記事の要点
  • カプセル化(Encapsulation): データとそれを操作するメソッドを 1 つのクラスにまとめ、外部から内部状態を直接いじれないように隠す
  • 必要性: 変更耐性(内部実装を変えても利用側に影響しない) / 不整合防止(値の制約をクラス内で保証)
  • アクセス修飾子: private(クラス内のみ) / protected(継承先まで) / public(誰でも)
  • 実装パターン: getter / setter / immutable オブジェクト / Property(C#) / record(Java 14+) / readonly(PHP 8.1+)
  • カプセル化により 依存性の影響範囲がクラス内に限定される → 大規模開発で必須

カプセル化とは何か

カプセル化はオブジェクト指向の 3 大原則(カプセル化・継承・ポリモーフィズム)の 1 つ。データ(フィールド)とそれを操作するメソッドを 1 つのクラスにまとめ、外部からの直接操作を禁止する仕組みです。

カプセル化しないとどうなる?

// ❌ カプセル化なし: 全部 public
class BankAccount
{
    public float $balance = 0;
}

$acc = new BankAccount();
$acc->balance = -1000000;   // 残高マイナスを誰でも設定可能!
$acc->balance = 'abc';      // 文字列代入も止められない!(型なし)

こうなると呼び出し側が不正な値を入れ放題。データの整合性をどこで担保するか不明になり、バグの温床に。

カプセル化したクラス

class BankAccount
{
    private float $balance;

    public function __construct(float $initial = 0)
    {
        if ($initial < 0) {
            throw new InvalidArgumentException('初期残高は 0 以上');
        }
        $this->balance = $initial;
    }

    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 <= 0) {
            throw new InvalidArgumentException('出金額は正の数');
        }
        if ($amount > $this->balance) {
            throw new RuntimeException('残高不足');
        }
        $this->balance -= $amount;
    }
}

$acc = new BankAccount(1000);
$acc->deposit(500);
$acc->withdraw(2000);   // ← RuntimeException
// $acc->balance = -100; // ← コンパイル/実行エラー(private)

これで残高が必ず 0 以上であることが BankAccount 内で保証されます。利用側は内部実装を気にしなくて OK。

アクセス修飾子

修飾子アクセス範囲用途
privateそのクラス内のみ内部状態・補助メソッド
protectedそのクラス + サブクラス継承先で使わせたい実装
publicどこからでもAPI として公開する操作
(default / package-private)同一パッケージ内(Java のみ)関連クラス間の共有

言語別の書き方

// Java
public class User {
    private String name;
    private int age;

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

    public String getName() { return name; }
    public int getAge() { return age; }

    public void setAge(int age) {
        if (age < 0) throw new IllegalArgumentException();
        this.age = age;
    }
}
# Python: 慣習的に _ で「内部」を示す(強制ではない)
class User:
    def __init__(self, name: str, age: int):
        self._name = name
        self.__age = age      # __ は name mangling で _User__age に

    @property
    def name(self) -> str:
        return self._name

    @property
    def age(self) -> int:
        return self.__age

    @age.setter
    def age(self, value: int) -> None:
        if value < 0:
            raise ValueError('age must be >= 0')
        self.__age = value
// C#: Property で簡潔に
public class User
{
    public string Name { get; init; }   // 初期化時のみセット可
    public int Age { get; private set; } // 外部から読み取り専用

    public User(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public void Birthday() => Age++;
}

イミュータブル(不変)オブジェクト

究極のカプセル化: そもそも値を変更できないクラス。スレッドセーフ・キャッシュフレンドリ・バグが起きにくい。

// PHP 8.1+ readonly プロパティ
final class Money
{
    public function __construct(
        public readonly int $amount,
        public readonly string $currency,
    ) {}

    public function add(Money $other): Money
    {
        if ($this->currency !== $other->currency) {
            throw new InvalidArgumentException();
        }
        // 自身を変更せず新インスタンスを返す
        return new Money($this->amount + $other->amount, $this->currency);
    }
}

$a = new Money(100, 'JPY');
// $a->amount = 200;  // ← Error: Cannot modify readonly property

$b = $a->add(new Money(50, 'JPY'));   // $b = 150 JPY (a は変わらない)

Java の record

Java 14+ の record はイミュータブルなデータクラスを 1 行で書ける構文糖:

// 自動で getter / equals / hashCode / toString / コンストラクタ生成
public record Money(int amount, String currency) {
    // バリデーションは compact constructor で
    public Money {
        if (amount < 0) throw new IllegalArgumentException();
    }

    public Money add(Money other) {
        if (!currency.equals(other.currency)) throw new IllegalArgumentException();
        return new Money(amount + other.amount, currency);
    }
}

Money m = new Money(100, "JPY");
int a = m.amount();  // 自動 getter

カプセル化の効用(なぜ必要か)

効用具体例
変更耐性balance を int から BigDecimal に変えても、getBalance() を返り値変えるだけ。呼び出し側多数に影響しない
不整合防止残高マイナス・年齢負数・null など不正状態をクラス内で防ぐ
影響範囲の限定private フィールドの変更影響はそのクラス内に限定。grep する範囲が狭くなる
テスタビリティpublicな操作 (deposit/withdraw) 経由でしか状態が変わらないので、テストが書きやすい
並行性イミュータブルなら lock 不要でスレッドセーフ

アンチパターン: getter/setter を機械的に全フィールドに付ける

「private にして getter/setter を自動生成」はカプセル化していないのと同じ:

// ❌ アンチパターン
class User
{
    private string $name;
    private int $age;

    public function getName(): string { return $this->name; }
    public function setName(string $name): void { $this->name = $name; }
    public function getAge(): int { return $this->age; }
    public function setAge(int $age): void { $this->age = $age; }
}

// 結局 setName() で何でも入れられる → カプセル化の意味がない

正しいカプセル化は「ドメインの操作」をメソッドにすること。例: setAge() ではなく birthday() / changeAddress(Address) 等。

カプセル化と Tell, Don't Ask 原則

// ❌ Ask: 状態を聞いて外で判断
if ($order->getStatus() === 'PAID' && $order->getShippedAt() === null) {
    $order->setShippedAt(now());
    $shipping->send($order);
}

// ✅ Tell: オブジェクトに振る舞いを依頼
if ($order->canShip()) {
    $order->ship($shipping);
}

FAQ

Q: protected と private、どちらをデフォルトにすべき?
A: private。継承で使いたくなった時にだけ protected に上げる方が安全です(緩める方向は楽、絞る方向は破壊的)。

Q: Python は private が無いから意味ないのでは?
A: __name(name mangling)で実質的な隠蔽は可能。何より規約による契約が重要で、_x は「触らないで」のシグナルです。

Q: イミュータブルにすると遅くなりませんか?
A: オブジェクト生成コストは確かに増えますが、ロック不要・キャッシュ可能・推論しやすさで多くの場面で合計コストは下がることが多いです。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. オブジェクト指向の概念
  2. 継承の概念と必要性
  3. ポリモーフィズム(多様性)の概念と必要性
  4. 抽象クラスの概念と必要性
  5. インターフェースの概念と必要性
  6. カプセル化の概念と必要性