---
title: "時區換算 - 重新思考 Rails 架構"
date: 2024-07-26T00:00:00+08:00
publishDate: 2024-07-26T00:00:00+08:00
lastmod: 2024-06-02T17:03:47+08:00
tags: ["Rails","Domain-Driven Design","設計","Clean Architecture"]
series: "rethink-rails-architecture"
toc: true
permalink: "https://blog.aotoki.me/posts/2024/07/26/rethink-rails-architecture-timezone-convert/"
language: "zh-tw"
---


在整個系統的開發過程中，因為客戶的服務是世界性的，因此還需要考慮到時區問題。然而只是單純的時區換算並不會造成問題過於複雜，而是在時區的呈現上並不像我們平常理解的那樣。

<!--more-->

## 當地時間{#localtime}

大多數時候，我們在時間的呈現上不外乎會選擇以 UTC（Coordinated Universal Time）或者當地時間（UTC+8）等方式「統一呈現」然而客戶的「當地時間」指的是某個欄位的當地時間。

舉例來說，我們從桃園機場 16:00 出發到沖繩，經過 90 分鐘飛行，在 18:30 抵達。在時間的表示上，實際上會是桃園機場的 16:00（UTC+8）經過 90 分鐘飛行，在 17:30（UTC+8）抵達，但在航空的習慣上都會用「當地時間」描述，因此我們要替換成 18:30（UTC+9）表示。

這就讓大部分後端服務的時間套件很難發揮作用，因為同一個畫面上要呈現的時間都是不一樣的。

## Value Object

在這類型的問題 Value Object（值物件）就會是一個很不錯的解決方案，如果你對於 Rails 有一定的開發經驗，大概都會聽過。

簡單來看 Value Object 的職責，就是根據系統的需要定義一些無法用整數、字串等基礎型別去表示的資料，那麼在這個狀況下，上述的時間議題就會變成類似這樣的物件。

```ruby
class LocalTime
  SITE_TIMEZONES = {
    TPE: 'Asia/Taipei'
    # ...
  }.freeze

  attr_reader :hour, :minute, :site

  def initialize(hour, minute, site)
    @hour = hour
    @minute = minute
    @site = site
  end

  def timezone
	SITE_TIMEZONES[site]
  end

  def zone
    @timezone ||=
	    ActiveSupport::TimeZone.new(timezone)
  end

  def to_time
    # this example assumption always same day
    zone.parse("#{hour}:#{minute}")
  end
end
```

搭配上 ActiveRecord 的 `composed_of` 機制，一定程度上可以讓維護這些資訊變得容易維護。

```ruby
class Order
  composed_of :depature_time, class_name: "LocalTime",
                              mapping: { depature_hour: :hour, ... }
  composed_of :arrival_time, class_name: "LocalTime",
                            mapping: { arrival_hour: :hour, ... }

  # ...
end

order = Order.new(
  depature_hour: 10,
  # ...
)

order.depature_time
# => #<LocalTime hour=10, ...>
order.depature_time.to_time
# => Fri, 26 July 2024 10:00:00.000000000 CST +08:00
```

## 資料查詢 {#data-queries}

大部分的系統很常會有需要查詢、篩選的需求，雖然我們利用 Value Object 的方式解決了呈現、運算上的問題，卻無法解決查詢的需求。

假設我們在台北的工作人員希望知道 10:00（UTC+8）出發的列表，那麼一筆當地時間為 11:00（UTC+9）的資料是不能用下面的 SQL 查詢出來的。

```sql
SELECT * FROM orders WHERE departue_hour = 10 " AND depature_site = 'TPE'
```

因為我們並不能確定不同時區的 `depature_site` 和 `depature_hour` 是相同的，假設需要可以查詢，那麼我們還需要在保存到資料庫時統一轉換成 UTC 時間，才能得到相同的篩選條件。

這一連串的需求處理下來，在當時嘗試了各種方法去處理這些情境，仍然很難的把事情乾淨的處理好，造成維護上非常困難。

