ValueObjectとは
ValueObjectとは、値をオブジェクトとして扱うパターンのこと(ルールあり。後記載)
ValueObjectを扱う前に
基本データ型との比較をしていき、なぜオブジェクトして扱うと良い場合があるのかを記載
例えば氏名を表す場合
基本データ型の場合
<?php
//姓だけを表示したい場合
$fullName = "satou takeru";
$nameArr = explode(" ", $fullName);
echo $nameArr[0];//satou
//以下の場合
$fullName = "tom cruise";
$nameArr = explode(" ", $fullName);
echo $nameArr[0];//tom
//↑間違ってしまう 英語の場合、名・姓の順番なので姓はcruiseだがtomになってしまう
オブジェクトで扱う場合
<?php
declare(strict_types=1);
/**
* @property-read string $firstName
* @property-read string $lastName
*/
final class FullName
{
/**
* @param string $firstName
* @param string $lastName
*/
public string $firstName;
public string $lastName;
public function __construct(
string $firstName,
string $lastName
) {
$this->firstName = $firstName;
$this->lastName = $lastName;
}
}
//同じく性だけを表示したい場合
$fullName = new FullName('takeru', 'satou');
echo $fullName->lastName;//satou
$fullName = new FullName('tom', 'cruise');
echo $fullName->lastName;//cruise
- 値をオブジェクトとして表すことで正しく扱えるようになった
- このように、複数の値を1つの値として扱いたい場合オブジェクトにすることで便利
では氏名とかでない場合は基本データ型でいいのか?
- int型で扱う場合
約-20億~+20億の範囲を取り扱う
- string型で扱う場合 (例: 固定電話番号の値をstring型で扱う場合)
固定電話番号のルール
- 使える文字種は数字だけ
- 市外局番は0で始まる
- 合計の桁数は10桁
- 形式は、(0-市外局番-市内局番-加入者番号)
- 加入者番号は4桁固定
- 先頭の”0”を除いた市外、市内局番は1~4桁
これをstring型で宣言した場合
- 文字の種類は数字だけでなく漢字でも記号でも何でも良い
- 長さは制限なし
- 形式は自由
問題点
- 業務で取り扱う範囲よりもかなり広い範囲のことを取り扱ってしまう
- 予期せぬ値や不正な値が入る恐れがあり、バグが生まれる可能性が高まる
...valueobjectだと改善されるか?
ここでValueObjectの特徴やルール
ルールや特徴 | 説明 |
---|---|
不変である | インスタンス生成後、値を変更させない。setterを用意しない |
交換可能 | 値を変更したい場合は、再生成する |
等価性 | 値が等しいか比較できる |
値のルール記述 | コンストラクタ内で値の不正チェックを行える |
知識の説明や計算が可能 | 振る舞いを持たせることができる |
などがあります
上記を基にFullNameクラスをValueObjectにしたコード
<?php
declare(strict_types=1);
/**
* @property-read string $firstName
* @property-read string $lastName
*/
final class FullName
{
/**
* @param string $firstName
* @param string $lastName
*/
public string $firstName;
/**
* @var string
*/
public string $lastName;
/**
* @param string $firstName
* @param string $lastName
*/
private function __construct(
string $firstName,
string $lastName
) {
//空文字の場合例外
//アルファベット以外が含まれている場合例外
$this->firstName = $firstName;
$this->lastName = $lastName;
}
/**
* @param string $firstName
* @param string $lastName
* @return FullName
*/
public static function of(
string $firstName,
string $lastName
): FullName {
return new FullName($firstName, $lastName);
}
/**
* @param string $firstName
* @param string $lastName
* @return bool
*/
public function equals(string $firstName, string $lastName): bool
{
return $this->firstName === $firstName && $this->lastName === $lastName;
}
}
$fullName = FullName::of('takeru', 'satou');
echo $fullName->firstName;//名前だけ表示
echo $fullName->equals('takeru', 'satou');//値が等しいか比較
先ほどの例であった電話番号の値もLandline(固定電話)型としてクラスを宣言し、形式や桁数のルールを記載していくようにする。
そして電話番号を扱う場合はこのLandline型を使うようにする。
そうすると不正な値が入ることはできないので、バグが生まれにくい作りにすることができる
不変にする理由
- 下記の理由もあり、予期しない値の変更を防ぐため
PHP では通常は値による代入になりますが、 オブジェクトは参照で代入されます。(PHPマニュアル)
不変ではないコードの例
<?php
declare(strict_types=1);
final class Dollar
{
private int $amount;
public function __construct(int $amount)
{
$this->amount = $amount;
}
public function times(int $multiplier): int
{
return $this->amount *= $multiplier;
}
public function setAmount(int $amount)
{
$this->amount = $amount;
}
public function value()
{
return $this->amount;
}
public function equals(Dollar $dollar): bool
{
return $this->amount === $dollar->amount;
}
}
$five = new Dollar(5);
$product = $five->times(2); //10になる
var_dump($product);
$product = $five->times(3);// 15になるべき が 30になってしまう
var_dump($product);
ValueObjectを不変にする方法
- インスタンス変数はコンストラクタでオブジェクトの生成時に設定する
- インスタンス変数を変更するメソッド(setter)を作らない
- 別の値が必要になれば、新しくインスタンス(オブジェクト)を作って返す
- 内部のインスタンス変数が変化しない不変なValueObjectは、ソフトウェアの副作用を減らし、バグを混入しにくくする
オブジェクトにすることでのメリットを増やしつつ、デメリットを限りなく減らす
ValueObjectの利点
表現力を増す
- 業務の用語やルールをそのままクラス名やメソッド名として使える
- コード自体が業務の説明書になる
- コードがわかりやすくなる
- もし業務ルールに変更があった場合でも変更が必要な場所を特定しやすい
不正な値を存在させない
- 値を生成する時に値チェックが可能である
- 事前にミスに気づける
誤った代入を防ぐ
- 不変であること
ロジックの散在を防ぐ
- 特定のクラスにデータとロジックが集まるので影響範囲をそのクラスに閉じ込めやすい
- この値を取り扱いたい場合はそのクラスを利用すればいいのでコードの重複がなくなる
ValueObjectにする基準は?
- 扱いたい値にルールや計算が存在しているか
- 複数の値を単体の情報として取り扱いたいか