#02 オブジェクト指向プログラミング入門

カプセル化——データを隠して安全にアクセスする

前回のおさらいと今回の問題

前回はクラスとオブジェクトの基本を学びました。public なプロパティを持つ Person クラスを作りましたが、実はあのコードには問題が潜んでいます。

class Person
{
    public function __construct(
        public string $name,
        public int $age,
    ) {}
}

$person = new Person('田中太郎', 30);

// 外部から直接変更できてしまう
$person->age = -5; // 年齢がマイナス?!
$person->name = ''; // 空文字の名前?!

プロパティが public だと、外部から自由に値を書き換えられます。「年齢がマイナス」「名前が空」といった不正な状態になってもクラスは気づけません。

これを防ぐのがカプセル化です。


アクセス修飾子の種類

PHPには3種類のアクセス修飾子があります。

修飾子アクセス可能な範囲
publicどこからでもアクセス可能
protectedそのクラス自身と子クラスからのみ
privateそのクラス自身からのみ

「情報を隠す」ことを情報隠蔽と呼び、カプセル化の核心です。外部に見せる必要のないデータは積極的に隠しましょう。


privateにしてゲッター・セッターを使う

プロパティを private にして、外部からのアクセスはメソッド経由に限定します。

class Person
{
    private string $name;
    private int $age;

    public function __construct(string $name, int $age)
    {
        // セッターを通すことでバリデーションが効く
        $this->setName($name);
        $this->setAge($age);
    }

    // ゲッター:値を読み取る
    public function getName(): string
    {
        return $this->name;
    }

    public function getAge(): int
    {
        return $this->age;
    }

    // セッター:値をセットする(バリデーション付き)
    public function setName(string $name): void
    {
        if (trim($name) === '') {
            throw new \InvalidArgumentException('名前は空にできません。');
        }
        $this->name = $name;
    }

    public function setAge(int $age): void
    {
        if ($age < 0 || $age > 150) {
            throw new \InvalidArgumentException('年齢が不正な値です。');
        }
        $this->age = $age;
    }
}
$person = new Person('田中太郎', 30);

// NG: 直接アクセスはできない
// $person->age = -5; // Fatal error

// OK: セッター経由なのでバリデーションが走る
$person->setAge(-5); // InvalidArgumentException がスローされる

セッターの中でバリデーションを行うため、不正な値が入り込む余地がありません。


なぜゲッターが必要か

private にしても結局ゲッターで全部読めるなら同じじゃないか」と思うかもしれません。しかしゲッターには大きなメリットがあります。

返す値を加工できる

class Person
{
    private string $firstName;
    private string $lastName;

    public function __construct(string $firstName, string $lastName)
    {
        $this->firstName = $firstName;
        $this->lastName  = $lastName;
    }

    // フルネームを組み合わせて返す
    public function getFullName(): string
    {
        return "{$this->lastName} {$this->firstName}";
    }
}

$person = new Person('太郎', '田中');
echo $person->getFullName(); // 田中 太郎

プロパティに直接アクセスさせず、必要な形に整えて返せます。

将来の変更に強い

内部の実装を変えても、ゲッターのインターフェースを保てば呼び出し元のコードを変える必要がありません。


readonly プロパティ(PHP 8.1)

「一度セットしたら変えたくない」プロパティには readonly が便利です。

class User
{
    public function __construct(
        public readonly int $id,      // 一度だけ書き込み可能
        public string $name,
    ) {}
}

$user = new User(1, '山田花子');
echo $user->id; // 1

// $user->id = 2; // Fatal error: Cannot modify readonly property

readonly はコンストラクタでのみ値をセットでき、その後は読み取り専用になります。IDのような変わることのない値に適しています。


protectedの使い方

protected は「子クラスには公開するが、外部には隠す」ときに使います。継承を学ぶ次回以降で詳しく扱いますが、ここで簡単に紹介します。

class Animal
{
    protected string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }
}

class Dog extends Animal
{
    public function bark(): string
    {
        // 子クラスからは protected プロパティにアクセスできる
        return "{$this->name}がワンワン吠えた!";
    }
}

$dog = new Dog('ポチ');
echo $dog->bark(); // ポチがワンワン吠えた!

// $dog->name; // Fatal error: protected へは外部からアクセス不可

実際のバグ例とその改善

アクセス修飾子を適切に使わないと起きるバグを見てみましょう。

NG例:銀行口座クラス

// NG: 残高が直接書き換えられてしまう
class BankAccount
{
    public int $balance = 0;
}

$account = new BankAccount();
$account->balance = 1000000; // 不正操作し放題!

OK例:カプセル化された銀行口座クラス

// OK: 入出金はメソッド経由のみ
class BankAccount
{
    private int $balance;

    public function __construct(int $initialBalance = 0)
    {
        if ($initialBalance < 0) {
            throw new \InvalidArgumentException('初期残高はマイナスにできません。');
        }
        $this->balance = $initialBalance;
    }

    public function deposit(int $amount): void
    {
        if ($amount <= 0) {
            throw new \InvalidArgumentException('入金額は1円以上である必要があります。');
        }
        $this->balance += $amount;
    }

    public function withdraw(int $amount): void
    {
        if ($amount <= 0) {
            throw new \InvalidArgumentException('出金額は1円以上である必要があります。');
        }
        if ($amount > $this->balance) {
            throw new \RuntimeException('残高が不足しています。');
        }
        $this->balance -= $amount;
    }

    public function getBalance(): int
    {
        return $this->balance;
    }
}

$account = new BankAccount(1000);
$account->deposit(500);
$account->withdraw(200);
echo $account->getBalance(); // 1300

残高を直接操作できないため、入出金の記録や検証をすべてクラス内で管理できます。


まとめ

  • プロパティは原則 private にして外部からの直接アクセスを防ぐ
  • ゲッター・セッターを通じてバリデーションや加工ができる
  • readonly を使えばイミュータブルなプロパティが簡単に作れる
  • protected は継承先だけに公開したいときに使う

カプセル化は「隠す」ことが目的ではなく、不正な状態を作らせないことが本当の目的です。

次回は継承——親クラスのコードを子クラスが引き継いで再利用・拡張する仕組みを学びます。