extension Dateで日付計算をしてはいけない

やりがちな `Date` のアンチパターンが何故悪いのか解説します。Swiftを例にとっていますが、 `Date` 型は多くの言語で似た責務を持っているので、あまり言語を問わない記事内容といえます。

この記事は[DeNA Advent Calendar 2020](https://qiita.com/advent-calendar/2020/dena)の2日目の記事です。
筆者はtakasekといいます。DeNA SWETの仕様分析サポートチームで[形式手法のプロダクト開発への適用可能性を模索](https://swet.dena.com/archive/category/%E5%BD%A2%E5%BC%8F%E6%89%8B%E6%B3%95)したり、[ライブコミュニケーションアプリ Pococha](https://www.pococha.com/) のiOSアプリ開発チームで開発したりしています。


## extension Date のアンチパターン

Dateインスタンスから年月日を求めるのは `Calendar` `DateComponents` といくつもの型を経由する必要があり面倒です。それをドットアクセスひとつで簡単に書けるようにしてくれる便利なextensionを作りました。

```swift
extension Date {
    var year: Int {
        Calendar.current.component(.year, from: self)
    }
    var month: Int {
        Calendar.current.component(.month, from: self)
    }
    var day: Int {
        Calendar.current.component(.day, from: self)
    }
    var hyphened: String {
        String(format: "%04d-%02d-%02d", year, month, day)
    }
}
```

```swift
let now = Date()
now.year // 2020
now.month // 12
now.day // 2
now.hyphened // "2020-12-02"
```

よさそうに見えますね。
しかし、これは大抵の開発シーンでは悪い設計だといえます。

## なぜ悪いのか

`Calendar.current.component(...)` というアクセスが頻繁に発生するから？
文字列化に `DateFormatter` を使っていないから？
いいえ、そんな理由ではありません。もっと本質的な問題があります。
それは**日付計算が `Date` の責務を超えてしまう**という点です。

`Date` とはどういう型なのか、リファレンスを見てみましょう。
https://developer.apple.com/documentation/foundation/date

> A specific point in time, independent of any calendar or time zone.

`Date` は**いかなるカレンダーやタイムゾーンからも独立した、ある時間軸上の一点の表現**です。単なる unixtime のラッパーだと思ったほうがよいです。[^1]

[^1]: 正確には違います。unixtime は `1970-01-01 00:00:00 UTC` からの経過時間ですが、SwiftのDateは `2001-01-01 00:00:00 UTC` からの経過時間を示していることが [`NSDate` のリファレンス](https://developer.apple.com/documentation/foundation/nsdate)や[apple/swiftのソースコード](https://github.com/apple/swift/blob/aa3e5904f8ba8bf9ae06d96946774d171074f6e5/stdlib/public/Darwin/Foundation/Date.swift#L25-L28)から確認できます。

リファレンスをさらに読み進めると以下のように書かれています。

> The Date structure provides methods for comparing dates, calculating the time interval between two dates, and creating a new date from a time interval relative to another date. Use date values in conjunction with DateFormatter instances to create localized representations of dates and times and with Calendar instances to perform calendar arithmetic.

`Date` のメソッドはdate同士の比較やインターバルの計算のためにあります。文字列表現を得るためには `DateFormatter` と、暦計算のためには `Calendar` と組み合わせてくれとリファレンスは言っています。
より詳しく実務的な `Date` 周りの考え方・ベストプラクティスについては、以下の素晴らしい記事をご参照ください。

【Swift】Dateの王道 【日付】 - Qiita
https://qiita.com/rinov/items/bff12e9ea1251e895306

## 本当に悪いのか

そうはいってもリファレンスに書いてあるからダメっていうのは教条的すぎじゃないか。`now.hyphend` と書けば `2020-12-02` が得られるというのはシンプルだ。複雑性を考えなくてすむようにするのが関心の分離ということだろう。内部処理として `Calendar` が利用されていればいいだけで、利用者としてはそこまで知る必要はない。
そう思われるかもしれません。

具体的な問題を考えてみましょう。
この `hyphend` は「サーバになんらかの年月日を送るとき」に使われているとします。しかしあるときサーバで不具合が起こり、調査した結果 `0002-12-02` という年月日が特定のユーザーからのみ送られてきていることがわかりました。
問題の根本は、そのユーザーが端末を和暦設定にしていたことです。

```swift
    var year: Int {
        Calendar.current.component(.year, from: self)
    }
```

 `0002-12-02` の `0002` というのは令和2年のこと。 `Calendar.current` なので、日付表現が端末の設定に依存してしまっていたのですね。
しかし `hyphened` のコードをいくら眺めていてもこの問題には気づけません。


```swift
    var hyphened: String {
        String(format: "%04d-%02d-%02d", year, month, day)
    }
```

なぜなら `Calendar.current` に依存しているという知識が暗黙になってしまっているからです。暗黙の依存はコードの理解を阻み、再利用性を落とし、バグを埋め込み、改修のコストを上げる、厄介な代物です。
先ほどの主張をもう一度振り返ってみましょう。

> `now.hyphend` と書けば `2020-12-02` が得られるというのはシンプルだ。複雑性を考えなくてすむようにするのが関心の分離ということだろう。内部処理として `Calendar` が利用されていればいいだけで、利用者としてはそこまで知る必要はない。

これは勘違いだったのです。どういった `Calendar` が利用されているかというのは、じつは暗黙にしてはいけない知識でした。それを覆い隠すのは、シンプルではなく**本来必要な知識のオミット**でしかなかったのです。
Rich Hickey はしばしば勘違いしがちなこの「シンプル」の捉え方について素晴らしい言語化をしてくれています。孫引きにはなりますが、以下の日本語解説記事を引用します。

Clojureと「Simple Made Easy」 - 紙箱
http://boxofpapers.hatenablog.com/entry/simple_made_easy

> つまりRichのいう「シンプル」というのは、「ひとつのものに、複数のことがらを混ぜ込まない」ということなのです。
> ...
> シンプルというのは、まっすぐの糸のようなもので、シンプルでないものというのは、糸が絡まっている状態です。
> ...
> シンプルでないものを作ろうとしてるときに「おい、それは○○してる（シンプルじゃなくしてる）ぞ」と言えれば、シンプルさを保つ役に立ちそうです。
> そこでRichは「コンプレクト(complect)」という英語（自動詞）を提案しています。

`Date` と `Calendar` がコンプレクトしている状態は、シンプルじゃなさそうです。

## どうやったら悪い設計を避けられるのか

テストを書きましょう。

```swift
extension Date {
    var year: Int {
        Calendar.current.component(.year, from: self)
    }
    ...
}

XCTAssertEqual(now.year, 2020)
```

このテストケースを考えていると、
「Calendarの設定を変えてのテストが書きづらいな」
「そもそもCIの環境によって、同じDateインスタンスでも日付表現が変わってテストが落ちてしまうな」
ということに気付くはずです。

暗黙の依存への対策は、外から注入可能にすることです。

```swift
extension Date {
    func year(in calendar: Calendar = Calendar.current) -> Int {
        calendar.component(.year, from: self)
    }
    ...
}
let calendar = Calendar(identifier: .gregorian)
calendar.timeZone = ...
calendar.locale = ...

XCTAssertEqual(now.year(in: calendar), 2020)
```

これでテスト可能になりました…が、よく見るとこのコードは、ほとんど引数で受けたcalendarを使っているだけのコードです。だとしたらこれでいいじゃないですか。

```swift
extension Calendar {
    func year(of date: Date) -> Int {
        component(.year, from: date)
    }
    ...
}

XCTAssertEqual(calendar.year(of: now), 2020)
```

日付計算は `Date` ではなく `Calendar` のextensionにするのです。Dateは自分の実体であるtimestampのことだけを気にしていれば良くて、自分がCalendarに使われうることなんて知る必要はありません。それこそが関心の分離です。
これでコンプレクトが途端に消えて、「あるカレンダー上での現在の年」という意図がシンプルに伝わってくる、悪くない設計になりました！

## 誰が悪いのか

悪かった。悪い悪い言い過ぎました。
じつのところ、 `Date` で日付計算をしたくなってしまうあなたは悪くありません。
本当に悪いのはネーミングです。 `Date` という型名を見たらそりゃあ日付の知識があると思うのは自然です。
Dateで日付計算をしてはいけないし、**そもそも日付計算をしてはいけない型にDateとつけてはいけなかったんです**。

私は以前Twitterで、そういう会話をしたことがあります。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">これはそのとおり。このコードをみて TimePointという命名はよいなと思った。<a href="https://t.co/hiVFCQdpyQ">https://t.co/hiVFCQdpyQ</a> <a href="https://t.co/53HDmdTL0p">https://t.co/53HDmdTL0p</a></p>&mdash; かとじゅん (@j5ik2o) <a href="https://twitter.com/j5ik2o/status/1171959482902933504?ref_src=twsrc%5Etfw">September 12, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

いいですね、 `TimePoint` 。今からでも世界中のすべての `Date` が `TimePoint` にならないかな。

無理ですね。
ネーミングの議論をしていると、ときおり自転車置き場の議論に感じるかもしれません。どんな名前でもコンパイルは通りますからね。しかしネーミングというのは重要な作業です。名前が悪いと責務がぼやけて、本来はあるべきではない機能拡張が行われてしまいます。そして負債はずっと残り続けます。
私たちも、自分で型や関数を作るときには名前には拘りましょう。それが、責務の輪郭をはっきりとさせ関心を切り分けるための第一歩です。

## 最後に

この記事を読んで「面白かった」「学びがあった」と思っていただけた方、よろしければ Twitter や facebook、はてなブックマークにてコメントをお願いします！
また [DeNA 公式 Twitter アカウント @DeNAxTech](https://twitter.com/DeNAxTech) では、 Blog記事だけでなく色々な勉強会での登壇資料も発信してます。ぜひフォローして下さい！
<a href="https://twitter.com/DeNAxTech?ref_src=twsrc%5Etfw" class="twitter-follow-button" data-size="large" data-lang="ja" data-show-count="false">Follow @DeNAxTech</a><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>