加入聚合實體 - 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
這樣一來我們的資料上就已經調整成可以支援同時保存 amount
和 created_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>
到此為止,我們又再一次的通過測試,並且加入了新的功能。
因為是舉例的關係在實際開發時,建議再多加入幾個測試來驗證確保功能的完善,來確認是否涵蓋完整的關鍵案例。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 前言 - Rails 開發實踐
- 將需求實現的準備 - Rails 開發實踐
- 獲取規格的技巧 - Rails 開發實踐
- 可以測試的規格 - Rails 開發實踐
- 快速通過測試的方法 - Rails 開發實踐
- 用測試完善規格 - Rails 開發實踐
- 資料跟資訊的差異 - Rails 開發實踐
- 用測試資料驗證邏輯 - Rails 開發實踐
- 預期外狀況的檢查 - Rails 開發實踐
- 預期外狀況的測試 - Rails 開發實踐
- 聚合多筆資料 - Rails 開發實踐
- 重構與修正邏輯 - Rails 開發實踐
- 加入聚合實體 - Rails 開發實踐
- 持久化資料 - Rails 開發實踐
- 實體與倉庫 - Rails 開發實踐
- 聚合與邊界 - Rails 開發實踐
- 使用案例與服務 - Rails 開發實踐
- 結語 - Rails 開發實踐