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

加入聚合實體 - Rails 開發實踐

假設我們繼續確認訂閱的需求,發現現有的功能無法記錄使用者在何時進行延展,因此希望加入「在 2023-03-14 延展」的資訊在畫面上,然而我們現在是使用整數儲存在 Subscription#items 屬性中,除了無法明確表達意義外,也不容易再繼續擴充。

擴充測試

為了配合新的需求,這表示我們有新的規格出現,因此在開始實作新的機制之前,先加入新的測試來確認符合規則,因為要設定建立時間,因此我們也加入了一個新的步驟來反應這件事情。

1#language:zh-TW
2# ...
3  場景: Aotoki 有一筆 30 天的訂閱,會有一筆在 2023-01-01 開始的紀錄
4    假設 會員 Aotoki 從 2023-01-01 開始有一些訂閱紀錄
5      | amount | created_at |
6      | 30     | 2023-01-01 |
7     我打開訂閱狀態頁面
8    那麼 我會看到 "在 2023-01-01 延展"

根據這個新的步驟,我們還需要加入新的「步驟定義」去描述這件事情。

 1# ...
 2Given('會員 {word} 從 {word} 開始有一些訂閱紀錄') do |name, date, table|
 3  user_id = @users[name]
 4
 5  Subscription.create(user_id: user_id, started_at: Time.zone.parse(date))
 6  subscription = Subscription.by_user(user_id: user_id).first
 7  table.hashes.each do |row|
 8    subscription.extend_with(amount: row['amount'].to_i, created_at: Time.zone.parse(row['created_at']))
 9  end
10  subscription.save!
11end

這個實作基本上跟 假設 Aotoki 有兩筆 30 天的訂閱,可以看到 60 天後到期 這個步驟是差不多的,唯一不同的地方在於我們會將「起始日期」用於 Subscription 的起始時間,同時每一筆紀錄都可以額外的設定 created_at 的日期。

這裡我們利用 {word} 來匹配日期,為了更精確的表示,可以用 Cucumber 的 ParameterType 來登記一個新的參數類型。

加入 Subscription Item 實體

現有的 Subscription#items 是一個整數的陣列,這樣就無法去儲存 created_at 的資訊,這就是加入新的 Entity(實體)好機會。

1# app/models/subscription_item.rb
2
3class SubscriptionItem
4  include ActiveModel::Model
5  include ActiveModel::Attributes
6
7  attribute :created_at, :datetime, default: -> { Time.zone.now }
8  attribute :amount, :integer, default: 0
9end

有了 SubscriptionItem 的 Entity 後,我們要繼續修改 Subscription#extend_with 方法,相容新的步驟加入的 created_at 參數。

1# app/models/subscription.rb
2
3class Subscription
4  # ...
5  def extend_with(amount:, created_at: Time.zone.now)
6    items << SubscriptionItem.new(amount: amount, created_at: created_at)
7  end
8end

這樣一來我們的資料上就已經調整成可以支援同時保存 amountcreated_at 的資訊,然而在呈現的部分就會因為行為改變而無法正確使用,因此我們還需要再去調整呈現的部分。

修正顯示

這次受到影響的部分一共有兩個地方,其中一個是 SubscriptionHelper 無法直接用 items.sum 的方式。

1module SubscriptionHelper
2  def expired_in_days(subscription:)
3    expired_at = subscription.started_at + subscription.items.sum(&:amount).days
4    distance = [0, expired_at - Time.zone.now].max
5    (distance / 1.day).ceil
6  end
7end

在這裡做的修正需要改為 items.sum(&:amount) 的方式,這是利用 Ruby 對陣列操作的語言特性,可以利用 &:amount 來表示 items.sum { |item| item.amount } 的縮寫,來達到聚合處理的效果。

最後,我們還要讓顯示的部分加入「在 2023-01-01 延展」的顯示,並且修正原本直接印出整數的呈現。

 1<!-- app/views/subscriptions/index.html.erb -->
 2<div><%= expired_in_days subscription: @subscription %> 天後到期</div>
 3<table>
 4  <thead>
 5    <tr>
 6      <th>#</th>
 7      <th>天數</th>
 8    </tr>
 9  </thead>
10  <tbody>
11    <% @subscription.items.each_with_index do |item, idx| %>
12      <tr>
13        <td><%= idx %></td>
14        <td><%= item.created_at&.to_date %> 延展</td>
15        <td>延展 <%= item.amount %> 天</td>
16      </tr>
17    <% end %>
18  </tbody>
19</table>

到此為止,我們又再一次的通過測試,並且加入了新的功能。

因為是舉例的關係在實際開發時,建議再多加入幾個測試來驗證確保功能的完善,來確認是否涵蓋完整的關鍵案例。