【Android】DateからTemporalへのリファクタリング - アンチパターンからの脱却


Androidアプリ開発では、日付と時刻の操作は避けて通れない要素です。古くから使われてきた`java.util.Date`クラスは設計上の問題が多く、Java 8から導入された`java.time`パッケージに移行することで、より堅牢で読みやすいコードを実現できます。

この記事では、Kotlinを使用して`java.util.Date`から`java.time.temporal.Temporal`インターフェースを実装したクラス（主に`LocalDateTime`など）への移行方法を紹介します。

## 目次

- [なぜjava.util.Dateから移行すべきか？](#なぜjavautildateから移行すべきか)
- [基本的な変換方法](#基本的な変換方法)
- [実践的なリファクタリング例](#実践的なリファクタリング例)
  - [1. エンティティクラスのリファクタリング](#1-エンティティクラスのリファクタリング)
  - [2. Roomデータベースでのリファクタリング](#2-roomデータベースでのリファクタリング)
  - [3. 日付操作のリファクタリング](#3-日付操作のリファクタリング)
  - [4. 日付比較のリファクタリング](#4-日付比較のリファクタリング)
  - [5. 日付形式のフォーマット](#5-日付形式のフォーマット)
  - [6. APIからの日付解析](#6-apiからの日付解析)
  - [7. RecyclerViewでの日付表示](#7-recyclerviewでの日付表示)
  - [8. ユーティリティクラスの作成](#8-ユーティリティクラスの作成)
  - [9. テストコードの更新](#9-テストコードの更新)
  - [10. Kotlin拡張関数の活用](#10-kotlin拡張関数の活用)
  - [11. ViewModelでの実装例](#11-viewmodelでの実装例)
- [移行時の注意点](#移行時の注意点)
- [まとめ](#まとめ)

## なぜjava.util.Dateから移行すべきか？

`java.util.Date`には以下のような問題点があります：

1. **可変である**: 日付・時刻クラスは不変であるべきですが、`Date`は可変です
2. **混乱しやすい命名**: `getMonth()`が0始まりで1月が0になるなど直感的でない
3. **タイムゾーン処理の難しさ**: タイムゾーンの処理が複雑で混乱を招きやすい
4. **スレッドセーフでない**: 可変であるため複数スレッドでの処理に危険
5. **日付計算の複雑さ**: 日付の加算・減算に`Calendar`クラスが必要

一方、`java.time`パッケージには以下のメリットがあります：

1. **不変オブジェクト**: すべてのクラスが不変でスレッドセーフ
2. **目的別のクラス**: 用途に合わせた適切なクラスを選択可能
3. **明確なAPI**: メソッド名が直感的で使いやすい
4. **強力な機能**: 期間、日付範囲などの計算が容易
5. **フォーマットとパース**: 日付・時刻の解析と整形が簡単

## 基本的な変換方法

まず、基本的な変換方法を見てみましょう：

```kotlin
import java.util.Date
import java.time.LocalDateTime
import java.time.ZoneId

// 変更前：java.util.Date
val oldDate: Date = Date()

// 変更後：LocalDateTime (Temporalを実装)
val newDateTime: LocalDateTime = LocalDateTime.now()

// 変換方法
val convertedDateTime: LocalDateTime = oldDate.toInstant()
    .atZone(ZoneId.systemDefault())
    .toLocalDateTime()
```

## 実践的なリファクタリング例

### 1. エンティティクラスのリファクタリング

データクラスを日付型から変更する例です：

```kotlin
import java.util.Date
import java.time.LocalDateTime

// リファクタリング前
data class UserModel(
    val id: Long,
    val name: String,
    val createdAt: Date,
    val updatedAt: Date
)

// リファクタリング後
data class UserModel(
    val id: Long,
    val name: String,
    val createdAt: LocalDateTime,
    val updatedAt: LocalDateTime
)
```

### 2. Roomデータベースでのリファクタリング

Room使用時のTypeConverterの書き換え例です：

```kotlin
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.ColumnInfo
import androidx.room.TypeConverter
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.Instant

// リファクタリング前のエンティティ
@Entity(tableName = "users")
data class UserEntity(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    
    @ColumnInfo(name = "created_at")
    val createdAt: Date? = null
    
    // その他のフィールド...
)

// リファクタリング前のTypeConverter
class DateConverter {
    @TypeConverter
    fun fromTimestamp(value: Long?): Date? {
        return value?.let { Date(it) }
    }

    @TypeConverter
    fun dateToTimestamp(date: Date?): Long? {
        return date?.time
    }
}

// リファクタリング後のエンティティ
@Entity(tableName = "users")
data class UserEntity(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    
    @ColumnInfo(name = "created_at")
    val createdAt: LocalDateTime? = null
    
    // その他のフィールド...
)

// リファクタリング後のTypeConverter
class LocalDateTimeConverter {
    @TypeConverter
    fun fromTimestamp(value: Long?): LocalDateTime? {
        return value?.let {
            Instant.ofEpochMilli(it)
                .atZone(ZoneId.systemDefault())
                .toLocalDateTime()
        }
    }

    @TypeConverter
    fun localDateTimeToTimestamp(dateTime: LocalDateTime?): Long? {
        return dateTime?.atZone(ZoneId.systemDefault())
            ?.toInstant()
            ?.toEpochMilli()
    }
}
```

@Database注釈でTypeConverterを登録するのを忘れないでください：

```kotlin
@Database(entities = [UserEntity::class], version = 1)
@TypeConverters(LocalDateTimeConverter::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}
```

### 3. 日付操作のリファクタリング

日付計算の簡易化：

```kotlin
import java.util.Date
import java.util.Calendar
import java.time.LocalDateTime

// リファクタリング前
fun addDays(date: Date, days: Int): Date {
    val calendar = Calendar.getInstance()
    calendar.time = date
    calendar.add(Calendar.DAY_OF_MONTH, days)
    return calendar.time
}

// リファクタリング後
fun addDays(dateTime: LocalDateTime, days: Int): LocalDateTime {
    return dateTime.plusDays(days.toLong())
}
```

### 4. 日付比較のリファクタリング

日付の比較方法：

```kotlin
import java.util.Date
import java.time.LocalDateTime

// リファクタリング前
fun isDateBefore(date1: Date, date2: Date): Boolean {
    return date1.before(date2)
}

// リファクタリング後
fun isDateBefore(dateTime1: LocalDateTime, dateTime2: LocalDateTime): Boolean {
    return dateTime1.isBefore(dateTime2)
}
```

### 5. 日付形式のフォーマット

日付のフォーマット方法：

```kotlin
import java.util.Date
import java.text.SimpleDateFormat
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

// リファクタリング前
fun formatDate(date: Date): String {
    val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
    return sdf.format(date)
}

// リファクタリング後
fun formatDate(dateTime: LocalDateTime): String {
    return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
}
```

### 6. APIからの日付解析

日付文字列のパース：

```kotlin
import java.util.Date
import java.text.SimpleDateFormat
import java.text.ParseException
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

// リファクタリング前
fun parseApiDate(dateString: String): Date? {
    return try {
        val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
        sdf.parse(dateString)
    } catch (e: ParseException) {
        null
    }
}

// リファクタリング後
fun parseApiDate(dateString: String): LocalDateTime? {
    return try {
        LocalDateTime.parse(dateString, DateTimeFormatter.ISO_DATE_TIME)
    } catch (e: Exception) {
        null
    }
}
```

### 7. RecyclerViewでの日付表示

アダプターでの日付表示：

```kotlin
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import android.widget.TextView
import java.text.SimpleDateFormat
import java.time.format.DateTimeFormatter
import java.time.LocalDateTime

// リファクタリング前
class TaskAdapter(private val tasks: List<Task>) : 
    RecyclerView.Adapter<TaskAdapter.ViewHolder>() {
    
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val task = tasks[position]
        val dueDate = task.dueDate
        
        val sdf = SimpleDateFormat("MMM d, yyyy")
        holder.dueDateTextView.text = sdf.format(dueDate)
    }
    
    // その他のメソッド...
}

// リファクタリング後
class TaskAdapter(private val tasks: List<Task>) : 
    RecyclerView.Adapter<TaskAdapter.ViewHolder>() {
    
    private val formatter = DateTimeFormatter.ofPattern("MMM d, yyyy")
    
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val task = tasks[position]
        val dueDate = task.dueDate
        
        holder.dueDateTextView.text = dueDate.format(formatter)
    }
    
    // その他のメソッド...
}
```

### 8. ユーティリティクラスの作成

移行期用のユーティリティクラス：

```kotlin
import java.util.Date
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.Instant
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException

object DateTimeUtils {
    // Date → LocalDateTime
    fun toLocalDateTime(date: Date?): LocalDateTime? {
        return date?.toInstant()
            ?.atZone(ZoneId.systemDefault())
            ?.toLocalDateTime()
    }
    
    // LocalDateTime → Date
    fun toDate(dateTime: LocalDateTime?): Date? {
        return dateTime?.atZone(ZoneId.systemDefault())
            ?.toInstant()
            ?.let { Date.from(it) }
    }
    
    // ISO 8601形式の文字列を解析して日時を取得
    fun parseIsoDateTime(dateTimeStr: String?): LocalDateTime? {
        return try {
            dateTimeStr?.takeIf { it.isNotEmpty() }?.let {
                LocalDateTime.parse(it, DateTimeFormatter.ISO_DATE_TIME)
            }
        } catch (e: DateTimeParseException) {
            null
        }
    }
    
    // 日時をISO 8601形式の文字列に変換
    fun formatIsoDateTime(dateTime: LocalDateTime?): String? {
        return dateTime?.format(DateTimeFormatter.ISO_DATE_TIME)
    }
}
```

### 9. テストコードの更新

テストコードでの日付操作：

```kotlin
import org.junit.Test
import java.util.Date
import java.time.LocalDateTime
import junit.framework.Assert.assertTrue

// リファクタリング前
class TaskManagerTest {
    @Test
    fun testTaskIsDue() {
        val now = Date()
        val yesterday = Date(now.time - 24 * 60 * 60 * 1000)
        
        val task = Task().apply {
            dueDate = yesterday
        }
        
        assertTrue(taskManager.isOverdue(task))
    }
}

// リファクタリング後
class TaskManagerTest {
    @Test
    fun testTaskIsDue() {
        val now = LocalDateTime.now()
        val yesterday = now.minusDays(1)
        
        val task = Task().apply {
            dueDate = yesterday
        }
        
        assertTrue(taskManager.isOverdue(task))
    }
}
```

### 10. Kotlin拡張関数の活用

Kotlinの拡張関数を使った変換ユーティリティ：

```kotlin
import java.util.Date
import java.time.*

// Date拡張関数
fun Date.toLocalDate(): LocalDate = 
    this.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()

fun Date.toLocalDateTime(): LocalDateTime = 
    this.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime()

fun Date.toZonedDateTime(): ZonedDateTime = 
    this.toInstant().atZone(ZoneId.systemDefault())

// LocalDateTime拡張関数
fun LocalDateTime.toDate(): Date = 
    Date.from(this.atZone(ZoneId.systemDefault()).toInstant())

// LocalDate拡張関数
fun LocalDate.toDate(): Date = 
    Date.from(this.atStartOfDay(ZoneId.systemDefault()).toInstant())

// ZonedDateTime拡張関数
fun ZonedDateTime.toDate(): Date = 
    Date.from(this.toInstant())
```

### 11. ViewModelでの実装例

ViewModelでの日付操作の例：

```kotlin
import androidx.lifecycle.ViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import java.time.LocalDateTime
import java.time.LocalDate
import java.time.format.DateTimeFormatter

class EventViewModel : ViewModel() {
    
    private val _events = MutableLiveData<List<Event>>()
    val events: LiveData<List<Event>> = _events
    
    fun loadEventsForDate(date: LocalDate) {
        viewModelScope.launch {
            // LocalDateを使って日付範囲を指定
            val startOfDay = date.atStartOfDay()
            val endOfDay = date.plusDays(1).atStartOfDay().minusNanos(1)
            
            val result = repository.getEventsBetween(startOfDay, endOfDay)
            _events.value = result
        }
    }
    
    fun formatEventTime(dateTime: LocalDateTime): String {
        // 時刻のみをフォーマット
        return dateTime.format(DateTimeFormatter.ofPattern("HH:mm"))
    }
    
    fun isEventSoon(event: Event): Boolean {
        val now = LocalDateTime.now()
        val eventTime = event.startTime
        
        // 開始時間まで1時間以内かどうか
        return now.isBefore(eventTime) && 
               now.plusHours(1).isAfter(eventTime)
    }
}
```

## 移行時の注意点

### 1. タイムゾーンの適切な処理

`LocalDateTime`はタイムゾーン情報を持たないため、タイムゾーンを意識する場合は`ZonedDateTime`を使用しましょう。

```kotlin
// タイムゾーンを明示的に指定
val tokyoZone = ZoneId.of("Asia/Tokyo")
val zonedDateTime = ZonedDateTime.of(localDateTime, tokyoZone)
```

### 2. nullの安全な扱い

Kotlinのnull安全性を活用して、null許容型と非null型を適切に使い分けましょう。

```kotlin
// nullの場合に代替値を提供
val dateTime: LocalDateTime? = nullableDateTimeValue
val safeDateTime = dateTime ?: LocalDateTime.now()
```

### 3. 使用する場面に応じたクラスの選択

`java.time`パッケージには様々なクラスがあります。用途に応じて適切なものを選びましょう。

- `LocalDate`: 日付のみ（年、月、日）
- `LocalTime`: 時刻のみ（時、分、秒、ナノ秒）
- `LocalDateTime`: 日付と時刻
- `ZonedDateTime`: タイムゾーン付きの日付と時刻
- `Instant`: エポック秒からの経過時間を表す
- `Duration`: 時間ベースの期間
- `Period`: 日付ベースの期間

### 4. テキスト表現とフォーマット

あらかじめ定義されたフォーマッタを活用しましょう。

```kotlin
// 定義済みフォーマッタ
val isoFormatter = DateTimeFormatter.ISO_DATE_TIME
val basicFormatter = DateTimeFormatter.BASIC_ISO_DATE

// カスタムフォーマッタ
val customFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日")
```

### 5. データ移行の計画

既存のアプリでは、一気に移行するのではなく、以下のような段階的なアプローチがおすすめです：

1. 変換ユーティリティを作成する
2. 新しいコードではすべて新APIを使用する
3. 少しずつ古いコードをリファクタリングする
4. テストを充実させて移行の正確性を確認する

## まとめ

`java.util.Date`から`java.time`パッケージへの移行は、アプリケーションのコード品質と保守性を向上させるための重要なステップです。Kotlinの機能を活用することで、より安全で読みやすいコードを実現できます。

日付・時刻処理は多くのアプリケーションで中心的な役割を果たすため、このリファクタリングの投資効果は大きいでしょう。特に、Kotlinの拡張関数と非null型の活用は、コードをさらに改善する強力なツールになります。

リファクタリングは一度に行う必要はありません。段階的に進めながら、その過程で得た知見をチームで共有していくことをお勧めします。

## 参考リンク

- [Java 8 Date Time API](https://docs.oracle.com/javase/8/docs/api/java/time/package-summary.html)
- [Kotlin公式ドキュメント](https://kotlinlang.org/docs/home.html)
- [AndroidデベロッパーガイドRoom](https://developer.android.com/training/data-storage/room)