[Laravel5.8] CarbonのImmutable版が使えると何が嬉しいのか
こんにちは、ナカエです。
Laravel5.8 で日時操作ライブラリのCarbonの1系と2系を使い分けることができるようになるそうです。
この変更によりCarbonのImmutable(イミュータブル/不変)版が利用可能になります。
Carbon Updates Coming to Laravel 5.8
これまでは、Carbonの代わりにChronosを使ったり、独自のImmutableな日時操作クラスを作ったりしていたので、この変更は非常に助かります。
Immutableとは
オブジェクトがImmutable(不変)であるとは、一度オブジェクトが作成された後にメソッドを呼んでも内部状態が変わらない性質があるということです。逆に、内部状態が変更可能なオブジェクトの性質をMutableと呼びます。
ご存知の方には当然のことですが、オブジェクトがImmutableだと何が嬉しいのでしょうか? CarbonのMutable版とImmutable版の違いを通して見ていきましょう。
Mutableな処理の落とし穴
従来のCarbonは、Carbon::addDay()など日時を操作するメソッドの返り値として、内部の値を変化させたそのオブジェクト自身を返していました。
以下、現在日時に変更操作を加え、現在日時と変更後の日時を表示するという単純なケースを考えます。
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
const DATE_FORMAT = 'Y/m/d H:i:s';
\Carbon\Carbon::setTestNow('2018-10-12T14:31:00');
$now = \Carbon\Carbon::now();
$tomorrow = $now->addDay();
echo "now: {$now->format(DATE_FORMAT)}, tomorrow: {$tomorrow->format(DATE_FORMAT)}" . PHP_EOL;
こうすると何が起こるでしょうか? 実行結果は下記のようになります。
実行結果:
now: 2018/10/13 14:31:00, tomorrow: 2018/10/13 14:31:00
なんということでしょう、元の現在日時まで変更後の値になってしまいました。これは、$nowと$tomorrowが同じオブジェクトであることによって引き起こされる悲劇です。
Carbon::copy()で状態の変化を防ぐ
\Carbon\Carbonで元の日時と変更後の日時の両方を扱いたい場合は、一度オブジェクトを複製する必要があります。
\Carbon\Carbon::setTestNow('2018-10-12T14:31:00');
$now = \Carbon\Carbon::now();
$tomorrow = $now->copy()->addDay();
echo "[Mutable copy] now: {$now->format(DATE_FORMAT)}, tomorrow: {$tomorrow->format(DATE_FORMAT)}" . PHP_EOL;
実行結果:
[Mutable copy] now: 2018/10/12 14:31:00, tomorrow: 2018/10/13 14:31:00
copy()により元のオブジェクトは現在日時を指したままになっています。このような防衛的な対応が可能とはいえ、人間たまにはうっかり忘れてしまうことがあります。
Immutable版
Immutable版のCarbonを使えば、Mutable版で起こりがちなミスを防ぐことができます。
\Carbon\CarbonImmutable::setTestNow('2018-10-12T14:31:00');
$nowImm = \Carbon\CarbonImmutable::now();
$tomorrowImm = $nowImm->addDay();
echo "[Immutable] now: {$nowImm->format(DATE_FORMAT)}, tomorrow: {$tomorrowImm->format(DATE_FORMAT)}" . PHP_EOL;
実行結果:
[Immutable] now: 2018/10/12 14:31:00, tomorrow: 2018/10/13 14:31:00
日時操作はImmutableなオブジェクトの強みがわかりやすい代表的な例です。一般的にもオブジェクトの設計をImmutableにすることにより、人間が意識しづらいオブジェクトの内部状態が原因となって引き起こされるバグを減らすことができます。Immutableな操作の欠点として、オブジェクトによるメモリの利用量は増えますが、バグが減ることに比べれば多くの場合瑣末です。
Mutable版とImmutable版の違い
最後に、Mutable版である\Carbon\CarbonとImmutable版である\Carbon\CarbonImmutableの違いをまとめておきます。
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
// Mutable
$now = \Carbon\Carbon::now();
$tomorrow = $now->addDay();
assert($now instanceof DateTimeInterface); // DateTimeInterfaceの実装である
assert($now instanceof DateTime); // DateTimeを継承している
assert($now === $tomorrow); // 変更系のメソッドの返り値が同じオブジェクトを指す
assert($tomorrow->diff($now)->format('%D') === '00'); // 元の変数を安易に使うと値が変わっているので悲劇が起こる
assert(\Carbon\Carbon::isMutable() === true); // 静的な判定メソッドがある
assert(\Carbon\Carbon::isImmutable() === false);
assert($now->isMutable() === true); // インスタンスからも利用可能
assert($now->isImmutable() === false);
assert($now->toImmutable() instanceof \Carbon\CarbonImmutable); // Immutableに変換可
// Immutable
$nowImm = \Carbon\CarbonImmutable::now();
$tomorrowImm = $nowImm->addDay();
assert($nowImm instanceof DateTimeInterface); // DateTimeInterfaceの実装である
assert($nowImm instanceof DateTimeImmutable); // DateTimeImmutableを継承している
assert($nowImm !== $tomorrowImm); // 更系のメソッドの返り値として新しくオブジェクトが生成される
assert($tomorrowImm instanceof \Carbon\CarbonImmutable); // 新規作成されたオブジェクトもまたImmutable
assert( $tomorrowImm->diff($nowImm)->format('%D') === '01'); // 元の変数を使っても違う値なので安心
assert(\Carbon\CarbonImmutable::isMutable() === false); // 静的な判定メソッドがある
assert(\Carbon\CarbonImmutable::isImmutable() === true);
assert($nowImm->isMutable() === false); // インスタンスからも利用可能
assert($nowImm->isImmutable() === true);
assert($now->toMutable() instanceof \Carbon\Carbon); // Mutableに変換可
Carbon自体はLaravelとは独立したライブラリですので、もちろんLaravelアプリ以外のプロジェクトでも利用できます。CarbonImmutableを使って快適な日時操作ライフを送りましょう。