蒼時弦也
蒼時弦也
資深軟體工程師
發表於

時區換算 - 重新思考 Rails 架構

這篇文章是 重新思考 Rails 架構 系列的一部分。

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

當地時間

大多數時候,我們在時間的呈現上不外乎會選擇以 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 的職責,就是根據系統的需要定義一些無法用整數、字串等基礎型別去表示的資料,那麼在這個狀況下,上述的時間議題就會變成類似這樣的物件。

 1class LocalTime
 2  SITE_TIMEZONES = {
 3    TPE: 'Asia/Taipei'
 4    # ...
 5  }.freeze
 6
 7  attr_reader :hour, :minute, :site
 8
 9  def initialize(hour, minute, site)
10    @hour = hour
11    @minute = minute
12    @site = site
13  end
14
15  def timezone
16	SITE_TIMEZONES[site]
17  end
18
19  def zone
20    @timezone ||=
21	    ActiveSupport::TimeZone.new(timezone)
22  end
23
24  def to_time
25    # this example assumption always same day
26    zone.parse("#{hour}:#{minute}")
27  end
28end

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

 1class Order
 2  composed_of :depature_time, class_name: "LocalTime",
 3                              mapping: { depature_hour: :hour, ... }
 4  composed_of :arrival_time, class_name: "LocalTime",
 5                            mapping: { arrival_hour: :hour, ... }
 6
 7  # ...
 8end
 9
10order = Order.new(
11  depature_hour: 10,
12  # ...
13)
14
15order.depature_time
16# => #<LocalTime hour=10, ...>
17order.depature_time.to_time
18# => Fri, 26 July 2024 10:00:00.000000000 CST +08:00

資料查詢

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

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

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

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

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