値オブジェクトを使わず、プリミティブな型を使うとどうなるか
ブログシステムを考えましょう。
- ユーザーがいて、ユーザIDで区別する
- 記事があって、記事IDで区別するとします
両方とも int だとします。
<?php $userId = 1234; $articleId = 5678;
ArticleRepository
があり、ブログ記事を削除する場合、こんな感じになります。
<?php class ArticleRepository { public function delete(int $articleId) { // 記事を削除する処理 } } $userId = 1234; $articleId = 5678; /** @var ArticleRepository $articleRepository */ $articleRepository->delete($articleId);
このコードにはどんな問題点があるでしょうか。
各IDの型が分かりづらい
こういう事ができてしまいます。
<?php $articleId = 'abs1234';
$articleId
の型定義がどこにもないからです。
$articleRepository->delete($articleId);
を呼び出して初めて int
型であることに気づくでしょう。
delete メソッドの引数の型チェックが弱い
こういうことが起こってしまいます。
$userId = 1234; $articleId = 5678; /** @var ArticleRepository $articleRepository */ $articleRepository->delete($userId);
間違えて、deleteメソッドに userId を入れてしまっています。しかし、これはエラーにはなりません。代わりに、記事ID 1234 の記事が消えてしまいます。
そこで値オブジェクト
値オブジェクト、英語では ValueObject と呼ばれるものを PHP で導入するとこうなります。
<?php class UserId { /** @var int */ private $value; public function __construct(int $value) { $this->value = $value; } public function getValue(): int { return $this->value; } } class ArticleId { /** @var int */ private $value; public function __construct(int $value) { $this->value = $value; } public function getValue(): int { return $this->value; } }
これを利用すると、先程までの実装はこうなります。
<?php class ArticleRepository { // 型の制約が強くなる public function delete(ArticleId $articleId) { // 記事を削除する処理 } } $userId = new UserId(1234); $articleId = new ArticleId(5678); /** @var ArticleRepository $articleRepository */ $articleRepository->delete($articleId);
これで、
- userId は int であることが UserId クラスに定義されます
- 他に制約がある場合(マイナスの値はNG等)、ここに追加できます。
- userId と articleId を取り違えて
articleRepository
に渡すことはなくなります
さいごに
他にも値オブジェクトのありがたい点は色々あります。
以下の本が非常によくまとまっているので、読んでみてください。