2013-07-09 Tue 01:20
コード内で「現時刻」を気軽に取得してはいけない
日付を扱う処理についていろいろまとめたついでに、わりと簡単なことだけど知らないと落とし穴にハマる系のネタを。
日頃いろいろな処理を書いていて、現時刻を扱うこともは少なくないはずです。ですが、これを適当にやっていると困ることが多々あります。
実行中に「現時刻」を元にした処理が食い違う
例えばこんなコード。ログ集計とかやってるイメージです。
class Analyzer(object):
def analyze(self):
logfile = datetime.datetime.now().strftime('my_log_file.%H')
self.save(self.analyze_logfile(logfile))
def save(self, result):
now = datetime.datetime.now()
self.result[now.hour] = result
analyze_logsの実行にやたら時間がかかったり、やんごとない事情で14:57とかに実行して15:02に集計が終わった場合などに悲劇が起きます。
ここまで問題が明らかな場合に限らず、現時刻をその都度取得していると、そのスクリプトにとって「今」がいつを指し示すのかが曖昧になってしまいます。
この意識が薄いままコードを書く習慣が身についてしまうと、いざという時に上記のような問題を抱えたコードを書いてしまいがちなので気を付けたいところです。
この問題に対しては、例えば
def __init__(self):
self.target_time = datetime.datetime.now()
のように、最初に現時刻を取得しておいて、他の箇所ではこの時刻を使って処理します。
この時に現時刻を保存する変数名は、そのままnowとしても構いませんが、その時の処理に応じた適切な名前を付けましょう。上記の例では集計の対象時刻なのでtarget_timeとしています。
それが本当に「現時刻」である必要があるのか、それともたまたま現時刻を元にしているだけで、格納された値には別の意味があるのかの見極めは、設計にも大きな意味を持ちます。
ちなみに、話を分かりやすくするために省略しましたが、本当にログ集計を書く場合は対象時刻は現在ではなくローテート済みの「前の時間」なのでそのへんは調整が必要です。
再試行したい時に手が出せない
先のようなログ集計が代表例になります。特定の時間に対する処理をやり直したいとき、現時刻を取得するコードがあちこちにあると改修は大仕事になります。
先のように、現時刻を取得する処理が独立していればオプションで変更可能に拡張することもできるし、いざという時は直接コードを書き換えて緊急回避することだって可能です。
後から「あの時の処理」を追うのが難しい
特定の条件を満たしたプレイヤーにレアアイテムを配布する、というユースケースを想定します。
DBにアイテムのレコードをINSERTして、そのレコードには付与時刻のカラムがあったとしましょう。
例えば500件ずつBULK INSERTしたとして、付与時刻をINSERT文を発行する度に現時刻から取っていたら、一度のバッチで複数の「付与時刻」が記録されることになります。
後から「あの時にアイテム付与したユーザのidを全部くれ」と言われた時に、DBから取ってくる付与時刻は一定の期間で範囲検索することになります。また、その指定する範囲に漏れがないことを確実に保証するのは意外と厄介です。
「DBに入ったその瞬間がいつか」を記録したいという場面もあるかも知れませんが、それよりもひとつの意味を持つ処理単位で時刻が統一されていた方が便利なケースが多いのではないでしょうか。
どうせレプリケーションの遅延やらキャッシュやらでconsistencyを保証することが不可能なことも多いはずですし。
テスタビリティが落ちる
これは本質的な問題ではないと考える向きもあるかも知れませんが、重要なことだと考えています。
テストを書こうという意志を持っているのに、現時刻を扱うノウハウを持っていないために回りくどいコードを書いてしまったり、この部分のほころびが他にも波及してカバレッジが下がってしまったりという事例を見ています。
そういうことがあると「テスト書くの面倒だからTDDやりたくない」みたいな意識が生まれてしまいがちです。これは大変もったいないことです。
また「テストが書きにくい(書けない)」というのは、言い換えれば設計がよくないという見方もできます。そういうアプローチや考え方を採用するかは別にして、ノウハウとして持っておいて選択肢を増やすことは悪くないでしよう。
ここも先の構成であれば、テストコードで
analyzer = Analyzer()
analyzer.target_time = datetime.datetime(2013, 7, 8, 22, 43, 11)
このようにtarget_timeを変更するだけで自由にテストが書けます。
(追記)テストで現時刻を扱うためのライブラリも各言語で色々あります。PerlのTest::MockTimeや、Rubyのtimecop、Pythonだとそのものズバリは知らないけどmockでmonkey patchを当てることが出来ます。
これはこれで有効な手立てですが、こうしたライブラリに頼らずとも一貫した現時刻を扱えた方がいいでしょう。テストだけであればそうした回避策も有効ですが、最初からその部分が入れ替え可能な設計にしておいた方がより堅いアプローチだと考えます。
また、そういったライブラリでパッチを当ててテストを通した場合、そのテスト内では現時刻が固定されるため、先に挙げた問題を含んだコードがそのまま生き延びてしまうリスクが高まります。
現時刻をどこで取るべきか
簡単なアプリケーションであれば、上記のようにコンストラクタで現時刻を取れば解決するかも知れませんが、実際に書くアプリケーションの大半は一つのクラスで完結するものではないでしょう。じゃあ、どうするか。
原則として、現時刻を取得するのは「外部からの入力を受け取る部分」です。Facadeになるオブジェクトだけが現時刻を取得し、そこから呼ばれるドメイン層のオブジェクトは時刻を受け取るだけにするのが自分のパターンです。
具体的には、バッチであればCLIからパラメータ等を受け取るオブジェクト、WebアプリケーションであればRequestやContextに相当するオブジェクトがその役割を担います。
こうすることで「前はこのクラスで現時刻を取ってたが、その上に別のクラスが挟まったので、ここで現時刻を取っているのは具合が悪い」みたいな問題を回避することができます。
まとめ
- 現時刻をあちこちで取るな
- 入り口で取って使い回せ
- nowという名前が適切か考えろ