導入した経緯
システムには大なり小なりビジネスロジックが詰め込まれています。
同じ用語であっても時と場合によって指し示す対象が変わってきます。
その変化を吸収するためにシステムは常に改修が必要になりますが、そんな先のことまでは予知できないので、改修に改修を重ねているとシステムは複雑さが増して、同じコードが量産されていたり、どんどん設計が歪んでいきます。
Reluxもそんな状態に陥っており、設計のあるコードに軌道修正していこうとしています。
なぜValueObjectを使うのか
1. 返される値が保証される
チェックイン日データを利用した処理を書く上で、チェックイン日が"2017-12-21"
のような文字列なのかDateTime型なのかTimestamp(は、さすがにないか)なのか、ありえるデータ型を考慮するのは地味に手間がかかりがち。
必ずDateTime型と保証されていればロジックはシンプルに実装できます。
$checkin = $schedule->checkin(); // ← DateTimeImmutableで返ってくる
$today = new DateTimeImmutable('today');
if ($today < $checkin) {
// do something.
}
チェックイン日の値を保証するValueObjectは下記のような実装です。
チェックイン日とチェックアウト日からオブジェクトを生成し、宿泊日程に関する値に責務を持ちます。
class Schedule
{
/**
* @var DateTimeImmutable;
*/
private $checkin;
/**
* @var DateTimeImmutable;
*/
private $checkout;
public function __construct(DateTimeImmutable $checkin, DateTimeImmutable $checkout)
{
$this->checkin = $checkin;
$this->checkout = $checkout;
}
/**
* @return DateTimeImmutable
*/
public function checkin()
{
return $this->checkin;
}
(めっちゃDateTimeImmutableを主張していますね)
2. 値に対して行うことができる操作を保証する
クラスに値の加工メソッドが閉じ込められることで、そのクラスを見るだけで宿泊日程に関するロジックにどんなものがあるか分かります。
影響範囲が広がらず、安全に変更を加えることができます。
また責務が明確になるのでコードの重複を防ぐことができます。
/**
* @return int
*/
public function numberOfNights()
{
return (int)$this->checkout->diff($this->checkin)->format('%d');
}
例えば宿泊日数を返すメソッドですが、日数の計算も色んな方法があります。
この日数を利用する箇所に計算ロジックが分散し、ばらばらの方法で実装されてしまうことで、変更箇所が膨らんだり一部で誤差が発生するリスクがあります。
これをすべての箇所で利用するだけにしておくことでメンテナンス性を向上させることができます。
ReluxのValueObject規約
ValueObjectというのも設計上の概念でしかないので、明文化してチーム内で共通認識を持つことが重要です。
そのためにReluxの開発チームにおけるValueObjectの概念をこう定めています。
不変であること
- コンストラクタでのみ値を受け取る
- Setterは持たない。Getterしか持たない
- 扱う値が異なる時は別のインスタンスを生成する
- 1つのインスタンスで同じメソッドを呼び出すと、常に同じ値が返ってくる
DBモデルクラスを呼び出さない
- DBへのクエリを実行しない。クエリ実行タイミングによって値は可変である。使う場合はクエリで取り出した値でインスタンス化する
単一責任の原則(Single Responsibility Principle)を守る
- ValueObjectには処理フローを書かない。リクエストされた値を返すことだけに集中する
ちょっと油断するとクラスが肥大化するので気をつけたい。