カプセル化——データを隠して安全にアクセスする
前回のおさらいと今回の問題
前回はクラスとオブジェクトの基本を学びました。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は継承先だけに公開したいときに使う
カプセル化は「隠す」ことが目的ではなく、不正な状態を作らせないことが本当の目的です。
次回は継承——親クラスのコードを子クラスが引き継いで再利用・拡張する仕組みを学びます。