1
0

【PHP】PHPにおける不変クラス

Last updated at Posted at 2024-05-09

不変クラスとは

不変クラスとは、オブジェクトを作成した後に状態が一切変化しないクラスのことです。
クラスを不変にすることで、意図せずオブジェクトの状態が変更されてしまうことを防ぎます。
クラス設計においては、可能な限り不変クラスとすることが保守性の高いプログラムを組む上での近道となります。

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がディープコピー(値のコピー)を行うように設定します。

補足
$authorreadonlyのプロパティだから、$this->author = clone $this->author;は不可能に見えますが、PHPの仕様で__cloneの内部ではreadonlyプロパティの変更が可能になっています。(PHP8.3での変更)

おわりに

PHPで不変クラスを実現する方法について記載してみました。
記載の誤りなどがありましたらご指摘いただけると助かります。

参考文献

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0