不変クラスとは
不変クラスとは、オブジェクトを作成した後に状態が一切変化しないクラスのことです。
クラスを不変にすることで、意図せずオブジェクトの状態が変更されてしまうことを防ぎます。
クラス設計においては、可能な限り不変クラスとすることが保守性の高いプログラムを組む上での近道となります。
PHPにおける不変クラス
PHPでは近年、より堅牢なコードを記述できるようバージョンアップが重ねられており、不変クラスの設計も行いやすくなりました。
以下はBlog(ブログ)クラスを不変クラスとして記載したサンプルコードです。
なお、PHPのバージョンは現時点で最新の8.3を使用しています。
<?php
require_once 'Author.php';
final readonly class Blog {
private string $title;
private Author $author;
public function __construct(string $title, Author $author){
$this->title = $title;
$this->author = clone $author;
}
public function getTitle(): string {
return $this->title;
}
public function getAuthor(): Author {
return clone $this->author;
}
public function changeTitle(string $title): self {
return new Blog($title, clone $this->author);
}
public function __clone() {
$this->author = clone $this->author;
}
}
解説
1. final
キーワードで継承を禁止する
final readonly class Blog {
final
キーワードを付与することで、クラスが継承されることを禁止します。
継承はごく限られた条件でのみ使用されるべきで、濫用すると保守性を損ないます。
そのため、基本的にはfinal
キーワードを付与するようにします。
どのような場合に継承が許されるか
一般的に、以下の条件を満たす場合には継承をしてもよいとされます。
- リスコフの置換原則を満たしている(is-aの関係が成り立っている)
- 対象のクラスが継承されることを前提に設計されており、文書化されている
2. readonly
キーワードで読み取り専用にする
final readonly class Blog {
クラスにreadonly
キーワードを付与することで、クラス内のプロパティはすべて読み取り専用となります。(readonly class
はPHP8.2で追加された機能です)
また、readonly class
は動的プロパティも禁止します。
つまり、以下のようなコードはエラーになります。
$author = new Author('田中太郎');
$blog = new Blog(title: 'PHPにおける不変クラス', author: $author);
// Uncaught Error: Cannot create dynamic property Blog::$newProperty
$blog->newProperty = '新しいプロパティ';
動的プロパティはPHP8.2で非推奨となっています。
また、将来リリースされるPHP9.0ではエラーとなる予定です。
そのため、現時点から使用を禁止することをお勧めします。
3. プロパティのアクセス修飾子はprivate
にする
private string $title;
private Author $author;
不変クラスでは、外部から直接プロパティが参照されることを防ぐため、アクセス修飾子をprivate
にします。
プロパティの値を取得したい場合は、後述するgetterを使用します。
4. コンストラクタですべてのプロパティを初期化する
コンストラクタですべてのプロパティの初期化を実施します。
また、プロパティにオブジェクトを設定する必要がある場合、clone
でコピーしたものを設定します。
public function __construct(string $title, Author $author){
$this->title = $title;
$this->author = clone $author;
}
なぜclone
が必要か
PHPでは、オブジェクトは参照で渡されます。
上記の例で言うと$author
はオブジェクトの参照なので、呼び出し元で$author
が変更されると影響を受けてしまいます。
そのため、clone
でコピーしたオブジェクトをプロパティに設定します。
5. プロパティの値を取得する場合はgetterを使用する
public function getTitle(): string {
return $this->title;
}
public function getAuthor(): Author {
return clone $this->author;
}
前述の通り、プロパティはprivate
で宣言しているため外部から参照することはできません。
そのため、値を取得する必要がある場合はgetterを定義します。
ここでもオブジェクト($author
)の参照を直接渡してしまうと変更された場合に影響が及ぶので、clone
でコピーを渡すようにします。
getterの是非
getterはオブジェクトの内部状態を取得できてしまうため、getterの呼び出し元がオブジェクトの状態を使用して判断を実施したりしがちです。
こういった処理は「尋ねるな、命じろ」の格言に反しており、本来はオブジェクトの内部で判断を実施するべきです。
とはいえプロパティの値を取得して使用する機会はあるはずですので(別のデータ構造にデータを詰め替えたり、SQLにデータを埋め込んだり)、getterが必要になることもあると考えています。
6. プロパティの値を変更する必要がある場合は、新たなオブジェクトを作成して返す
public function changeTitle(string $title): self {
return new Blog($title, clone $this->author);
}
不変クラスとはいえ、特定のプロパティの値を変更したいケースは出てくると思います。
その場合には、変更した値を設定した新たなオブジェクトを返すようにします。
7. clone
された際の挙動を設定する
public function __clone() {
$this->author = clone $this->author;
}
PHPのclone
はシャローコピーです。
つまり、コピー対象のプロパティにオブジェクトが含まれている場合、その参照がコピーされます。
オブジェクトに変更があった場合、コピー元とコピー先が影響しあってしまいますので、__clone
マジックメソッドでclone
がディープコピー(値のコピー)を行うように設定します。
補足
$author
はreadonly
のプロパティだから、$this->author = clone $this->author;
は不可能に見えますが、PHPの仕様で__clone
の内部ではreadonly
プロパティの変更が可能になっています。(PHP8.3での変更)
おわりに
PHPで不変クラスを実現する方法について記載してみました。
記載の誤りなどがありましたらご指摘いただけると助かります。
参考文献