不正確さによる精度:時間オブジェクトの改善

時間を保存するための値オブジェクトを作成するときは、サブジェクト領域およびその周辺の専門家とともに、保存する精度を選択することをお勧めします。

数字で作品をモデル化することは、正確さを示すための良い形と考えられています。 それが何であるかは関係ありません-お金、サイズまたは重量; 指定された小数点以下の桁に丸めます。 この数値がユーザーへの表示専用である場合でも、丸めの存在により、データの処理と保存がより予測可能になります。







残念ながら、これは頻繁に行われることはなく、その時が来ると、問題はそれ自体で感じられます。 次のコードを検討してください。







$estimatedDeliveryDate = new DateTimeImmutable('2017-06-21'); // ,    2017-06-21 $now = new DateTimeImmutable('now'); if ($now > $estimatedDeliveryDate) { echo 'Package is late!'; } else { echo 'Package is on the way.'; }
      
      





6月21日に、このコードはPackage is on the way.



を出力する予定Package is on the way.



、その日はまだ終了しておらず、たとえば荷物は午後遅くに配達されるためです。







それにもかかわらず、コードはそうではありません。 時間の経過とともにパーツが指定されないため、PHPはnull値を慎重に置き換えて、 $estimatedDeliveryDate



2017-06-21 00:00:00



2017-06-21 00:00:00



2017-06-21 00:00:00





一方、 $now



... nowとして計算されます。 Now



時刻を含むようになりました。これは、おそらく真夜中ではないため、 2017-06-21 15:33:34



または2017-06-21 00:00:00



よりも後の2017-06-21 00:00:00









解決策1



「ああ、それは簡単に修正できます。」多くの人が言って、コードの必要な部分を更新します。







 $estimatedDeliveryDate = new DateTimeImmutable('2017-06-21'); $estimatedDeliveryDate = $estimatedDeliveryDate->setTime(23, 59);
      
      





クール、真夜中の前に時間を変更しました。 しかし、現在は23:59:00



まで時間が経過して23:59:00



ため、その日の最後の59秒でコードを実行すると、同じ問題が発生します。







「Brr、大丈夫。」-答えが続きます。







 $estimatedDeliveryDate = new DateTimeImmutable('2017-06-21'); $estimatedDeliveryDate = $estimatedDeliveryDate->setTime(23, 59, 59);
      
      





素晴らしい、今これは修正されました。







... DateTime



オブジェクトにマイクロ秒を追加するPHP 7.1にアップグレードするまで。 そのため、問題はその日の最後の数秒で発生します。 負荷の高い交通システムで作業しているときに偏見が強すぎたのかもしれませんが、ユーザーやプロセスはきっとこれにつまずくでしょう。 このバグを見つけて頑張ってください。 :-/







さて、マイクロ秒を追加します。







 $estimatedDeliveryDate = new DateTimeImmutable('2017-06-21') $estimatedDeliveryDate = $estimatedDeliveryDate->modify('23:59:59.999999');
      
      





そして今、それは動作します。







ナノ秒を取得するまで。







PHP 7.2。







わかりました、エラーの外観が非現実的になる瞬間まで、エラーをさらに減らすことができます。 この時点で、このアプローチが誤っていることは明らかです。 無限に割り切れる値を追求し、到達できないポイントにどんどん近づいていきます 。 別のアプローチを試してみましょう。







決定2



境界線の前の最後の瞬間を計算する代わりに、代わりに境界線の比較を確認しましょう。







 $estimatedDeliveryDate = new DateTimeImmutable('2017-06-21'); //    ,   ,       $startOfWhenPackageIsLate = $estimatedDeliveryDate->modify('+1 day'); $now = new DateTimeImmutable('now'); //    >  >= if ($now >= $startOfWhenPackageIsLate) { echo 'Package is late!'; } else { echo 'Package is on the way'; }
      
      





このオプションは機能し、1日中機能します。 このようなコードはより複雑に見えます。 このロジックを値オブジェクトまたは同様のものにカプセル化しない場合、アプリケーションのどこかでこれを間違いなくスキップします。







これを行っても、1つのタイプの操作(> =)のみが論理的で順次になり、残りは機能しません。 同等性チェックのサポートを実装する場合、別のタイプのデータを作成し、適切に機能するように調整する必要があります。 へえ。







最後に(おそらく私だけのために)このソリューションには、潜在的に欠落しているドメイン概念という形で不快な瞬間があります。 「LatePeriodRangeはありますか?DeliveryDeadlineはありますか?」 聞いてもらえますか 「パッケージが遅れていた...何かが起こるのか?専門家は期限について話さなかった、期限はないようだ。これはEstimatedDeliveryDateとどう違うのか?それでは?」 パッケージはどこにも行きません。 これは、ビルトインロジックの奇妙な機能であり、今では頭に残っています。







これは正しい答えを提供するのに最適なソリューションですが、それはあまり良いソリューションではありません。 他に何ができるか見てみましょう。







決定3



私たちの目標は、2日間を比較することです。 DateTime



オブジェクトcが一連の数字(年、月、日、時間、分、秒など)の形式にnow



たとDateTime



すると、その日の一部は正常に機能します。 問題は、追加のインジケータ(時間、分、秒)により始まります。 あなたは問題を解決するためのトリッキーな方法について議論することができますが、事実は残っています-時間コンポーネントは私たちのチェックに害を与えます。







私たちにとって時間帯だけが重要な場合、なぜこれらの追加の意味に耐えるのでしょうか? 翌日の移行のみが重要な場合は、追加の時間または分でビジネスルールのロジックを変更しないでください。







余分なゴミを捨てるだけです。







 //    ,   $estimatedDeliveryDate = day(new DateTimeImmutable('2017-06-21')); $now = day(new DateTimeImmutable('now')); //     if ($now > $estimatedDeliveryDate) { echo 'Package is late!'; } else { echo 'Package is on the way.'; } // ,      //   , PHP      function day(DateTimeImmutable $date) { return DateTimeImmutable::createFromFormat( 'Ym-d', $date->format('Ym-d') ); }
      
      





これにより、ソリューション1の内容の比較または計算がソリューション2の精度で簡素化されます。ただし、これは最もgliいオプションであり、この実装ではday()



呼び出しを忘れがちです。







このようなコードは、抽象化に簡単に変換できます。 サブジェクトエリアの状況がより明確になったため、明確になりました。配達時間について話すときは、時間についてではなくその日について話しているのです。 これらの両方により、コードは内部カプセル化の良い候補となります。







カプセル化



つまり、この値オブジェクトを作成しましょう。







 $estimatedDeliveryDate = EstimatedDeliveryDate::fromString('2017-06-21'); $today = EstimatedDeliveryDate::today(); if ($estimatedDeliveryDate->wasBefore($today)) { echo 'Package is late!'; } else { echo 'Package is on the way.'; }
      
      





コードの読み方をご覧ください。 次に、値オブジェクトを実装します。







 class EstimatedDeliveryDate { private $day; private function __construct(DateTimeInterface $date) { $this->day = DateTimeImmutable::createFromFormat( 'Ym-d', $date->format('Ym-d') ); } public static function fromString(string $date): self { //    YYYY-MM-DD   .. return new static(new DateTimeImmutable($date)); } public static function today(): self { return new static(new DateTimeImmutable('now')); } public function wasBefore(EstimatedDeliveryDate $otherDate): bool { return $this->day < $otherDate->day; } }
      
      





クラスを使用できるようにすると、 EstimatedDeliveryDate



は別のEstimatedDeliveryDate



とのみ比較され、精度が収束するという便利な制限が自動的に取得されます。







必要な精度での処理は1か所にあります。 消費者コードは、この作業とは関係ありません。







テストは簡単です。







また、処理タイムゾーンの集中ストレージに最適な場所があります(この記事では説明していませんが、重要です)。







1つのヒント: today()



メソッドを使用して、複数のコンストラクターを作成できることを示しました。 実際には、システムクロッククラスを追加し、そこからインスタンスを取得することをお勧めします。 単体テストを書く方がはるかに簡単です。 「実際の」バージョンは次のようになります。







 $today = EstimatedDeliveryDate::fromDateTime($this->clock->now());
      
      





不正確さによる精度



処理していたDateTime



の精度を下げることにより、いくつかの異なるタイプのエラーを削除できたことを理解することが重要です。 これを行わなかった場合、問題のあるすべての側面を処理する必要があり、ほとんどの場合、すべてがうまくいかない可能性があります。







適切な結果を得るためにデータの品質を下げることは直観に反するように思えるかもしれませんが、実際には、モデル化しようとしているシステムのより現実的なビューです。 私たちのコンピューターはピコ秒で動作しますが、私たちのビジネスは(ほとんどの場合)そうではありません。 さらに、 コンピューターはおそらく嘘をついています。







おそらく、開発者として、私たちは感じます。 考えられるすべての情報を保持しながら、より柔軟で有望であることをお勧めします。 結局、どの情報を捨てるかを決めるのは誰ですか? 真実は、情報が将来的にはお金を要する可能があるということですが、現在では、可能性のある未来が起こる前に情報を維持するためのコストに見合う価値があるでしょう 。 これはハードディスクのスペースのコストだけでなく、問題、人、時間、そしてエラーの場合には評判のコストです。 データをその最も完全な形式で処理すること自体が正当化される場合もありますが、場合によってはできることだけをやみくもに保存する価値はありません。







わかりやすくするために、利用可能な時間情報を単純に削除することはお勧めしません。







私は 、ドメインの専門家とともにタイムスタンプの精度を明確に選択することをお勧めします。 予想よりも高い精度を得ることができる場合、これによりエラーが発生し、複雑さが増します。 必要な精度が低くなると、ビジネスロジックの問題や障害も発生します。 重要なことは、予想される必要な精度レベルを決定することです。







また、ユースケースごとに個別に精度を選択します。 丸めは通常、システムクロックレベルではなく、値オブジェクト内で実装されます。 場所によっては、ナノ秒までの精度が必要ですが、誰かが1年しか必要としない場合があります。 正確さを正しくすると、コードが明確になります。







どこにでもあります



特定の種類のエラー、つまりチェックに必要な精度の不一致についてのみ説明したことに注意してください。 しかし、このアドバイスははるかに広範囲のエラーに適用されます。 これらすべてには触れませんが、それでもお気に入りの「残留」エラーに注意したいと思います。







 // ,   21 ,       28  $oneWeekFromNow = new DateTimeImmutable('+7 days'); //  28     $explicitDate = new DateTimeImmutable('2017-06-28'); // ,    ? var_dump($oneWeekFromNow == $explicitDate);
      
      





いいえ、それらは同じではありません。 $oneWeekFromNow



は現在の時刻も保存するのに対し、 $explicitDate



の値は00:00:00



です。 すごい。







上記の例では、主に時間と日付を比較する場合の精度について説明しましたが、精度のモデリングは任意の時間単位に拡張されます。 時間がかかるプランニングアプリケーションの数と、四半期ごとの精度が必要な財務アプリケーションの数を想像してください。







問題の調査を開始するとすぐに、時間の経過に伴う不確実性によって説明できるエラーの数を理解できます。 それらは間違ったチェックや不十分な設計の論理フレームのように見えるかもしれませんが、それに飛び込むと、写真がどのように見えてくるのかがわかります。







私の経験では、このクラスのエラーはテスト中に見過ごされることが多いことが示されています。 システムクロックを持つオブジェクトは(今のところ)おなじみのものではないため、現在の時刻を使用するコードのテストはもう少し複雑です。 多くの場合、テスト用のデータはシステムで取得した形式で提供されないため、エラーが発生します。







これは、PHPの特定のDateTime



ライブラリの問題ではありません。 先週このことについて書いたとき、Anthony FerraraはRubyの時間精度はオペレーティングシステムによって異なるが、データベースを操作するためのライブラリには固定レベルがあると述べました。 デバッグするのが楽しい







時間をかけて作業するのは難しいです。 時間の比較は2倍です。







精度レベルの選択



精度レベルを選択することが重要であると言って、正しいレベルを選択する方法については話しませんでした。 原則として、技術的なニーズに合わせてタイムスタンプの十分な精度を確保すると同時に、ドメインオブジェクトの精度のレベルを設定する必要があります。







ログ、イベントマーク、メトリックについては、必要に応じて詳細を選択します。 このようなデータは主に技術担当者に必要であり、彼らにとっては、デバッグ時に追加の精度が必要になることがよくあります。 また、システムまたはシリアルデータには高い精度が必要になる可能性があります。







ビジネス上の問題が発生した場合は、情報の必要な正確性について主題分野の専門家に相談してください。 それらは、現在使用されているものと将来必要なものとの間のバランスを取るのに役立ちます。 多くの場合、ビジネスロジックは、借用した知識を使用して操作する必要がある領域であるため、複雑さを軽減することは賢明な動きです。 現実のモデルとして直接モデルを構築するのではなく、有用なモデルを構築していることに注意してください。







人生では、これは、同じクラス内であっても、さまざまな精度の必要性につながります。 このアプリケーションクラスを検討してください。







 class OrderShipped { //   - (),     private $estimatedDeliveryDate; //   - (),     private $shippedAt; // Event sourcing ,     private $eventRecordedAt; }
      
      





複数レベルの精度を持つことが奇妙に思える場合、これらのタイムスタンプの使用方法が異なることを思い出させてください。 $shippedAt



$eventRecordedAt



さえ同じ「時間」を指しますが、コードの完全に異なる部分を参照します。







四半期、財務カレンダー、シフト、朝、日、または夕方での分割など、予想外の時間ブロックで動作するビジネスに出くわすことがあります。 これらの追加ユニットを使用すると、多くの興味深い経験が得られます。







変更要件



議論のもう1つの重要な部分は、将来ビジネスルールが変更された場合、当初の合意よりも高い精度が必要な場合、共同で決定し、蓄積されたデータをどう処理するかが明確になることです。 理想的には、これはビジネスを技術的な問題から救うでしょう。







これは難しくありません。「当初は登録日のみが必要でしたが、現在はオフィスが閉鎖される前に登録を確認するのに時間がかかります。」 簡単な解決策は、次の営業日の開始前に時間を設定することです。おそらく少数のアカウントが間違っていますが、ほとんどの場合は許容できます。 または単にゼロ。 または、18-00の後、サブスクリプションの終了日がtoday +1 year



tomorrow +1 year



なくtomorrow +1 year



に設定されている場合、会社には追加のビジネスルールがあります。 これについて彼らと話し合ってください。 人々が最初から議論に含まれている場合、人々はより活発で変化に忠実です。







より複雑なケースでは、システム内の他のデータに基づいたデータ復旧を参照してください。 おそらく、登録時間はログまたはメトリックに保存されます。 場合によっては、これを行うことが単に不可能であり、レガシーケースを転送するために新しいロジックを作成する必要があります。 しかし、すべてを計画することは不可能であり、ほとんどの場合、何が変わるかわかりません。 これが人生です。







時間の正確性についての私の結論:必要なものを使用し、それ以上はありません。







アプリケーション:完璧なソリューション



今後、固定精度を選択してクラスを使用することには、実際的な利点があると感じています。 時間の経過とともに動作する私の理想的なPHPライブラリは次のようになります。精度を示す一連の抽象クラス。これから値オブジェクトを継承し、比較時に使用します。







 class ExpectedDeliveryDate extends PointPreciseToDate { } class OrderShippedAt extends PointPreciseToMinute { } class EventGenerationTime extends PointPreciseToMicrosecond { }
      
      





正確性の問題をクラスに移し、決定に責任を負います。 setTime()



などのメソッドを必要な精度に制限し、 DateInterval



丸め、時間を操作するときに意味のあることをすべて実行できます。 値オブジェクトのほとんどのメソッドをカプセル化し、サブジェクト領域に必要なもののみを公開します。 さらに、このようにして、人々が自分で価値オブジェクトを作成することを奨励します。 とても。 たくさん。 値オブジェクト。 うん。







ライブラリがカスタム時間単位を簡単に定義できるようになると、ボーナスが得られます。







誰もこれをしましたか? 誰も本当に時間がありませんか?








All Articles