聚合多筆資料 - Rails 開發實踐
大多數的訂閱功能都會希望有紀錄的機制,假設我們也收到了同樣的需求。那麼,我們現在除了訂閱之外,還需要加入「訂閱紀錄」的設計,讓我們的使用者可以知道訂閱了多少次,或者可以手動延展訂閱。
加入規格
現有的規格已經無法滿足新的情境,因此我們需要加入新的規格進行驗證這件事情,繼續補充規格文件,加入新的條件。
1#language:zh-TW
2# ...
3場景: 假設 Aotoki 有兩筆 30 天的訂閱,可以看到 60 天後到期
4 假設 會員 Aotoki 有一些訂閱紀錄
5 | amount |
6 | 30 |
7 | 30 |
8 當 我打開訂閱狀態頁面
9 那麼 我會看到 2 次 "延展 30 天"
10 並且 我會看到 "60 天後到期"
這一次我們希望在訂閱狀態的畫面上,除了到期天數之外,還會有訂閱了幾次的紀錄。要實現這樣的機制,我們可以利用 Aggregate(聚合)的特性處理,如果有寫過 SQL 的話可能不陌生,像是 SELECT SUM(level) FROM player
就是一種聚合的使用。
紀錄延展
為了要實現這個功能,我們需要針對延展的時間進行紀錄,在開始實作之前我們可以思考如何在 Cucumber 中實現建立這類假資料的行為,在回去修改我們的 Model 行為。
1# features/step_definitions/subscription.rb
2
3# ...
4
5Given('會員 {word} 有一些訂閱紀錄') do |name, table|
6 user_id = @users[name]
7
8 Subscription.create(user_id: user_id, expired_at: 0.days.from_now)
9 subscription = Subscription.by_user(user_id: user_id).first
10 table.hashes.each do |row|
11 subscription.extend_with(amount: row['amount'].to_i)
12 end
13 subscription.save!
14end
15
16# ...
17
18Then('我會看到 {int} 次 {string}') do |count, text|
19 expect(page).to have_text(text, count: count)
20end
在定義會員有多個訂閱紀錄的步驟定義中,我們會幫使用者產生一個「馬上到期」的訂閱,然後用 #extend_with
方法加入兩筆訂閱紀錄。
在 Aggregate 的使用中,所有屬於 Aggregate 底下的物件都會由這個 Aggregate 物件所控管,在我們的例子中 Subscription
從原本的 Entity(實體)變成會管理多筆資料的 Aggregate 物件。
實作聚合
同樣的,我們要控制實作的數量來減少修改,因此我們可以像這樣調整 Subscription
來達到更少的調整,但是仍然可以獲得相同的效果。
1class Subscription
2 # ...
3
4 attribute :user_id, :integer
5 attribute :expired_at, :datetime, default: -> { 30.days.from_now }
6 attribute :items, array: true, default: -> { [] }
7
8 def expired_in_days
9 (([0, expired_at - Time.zone.now].max + items.sum.days) / 1.day).ceil
10 end
11
12 def extend_with(amount:)
13 items << amount
14 end
15
16 # ...
17end
我們加入了 items
屬性到 Subscription
之中,當我們使用 #extend_with
方法時就會插入一筆資料到裡面,同時也將 #expired_in_days
做了修改,在計算完以 expired_at
為基準的日期差之後,會再額外加上延展的日期。
最後,我們更新 Controller 和加入 View 就可以通過這次新增的測試。
1# app/controllers/subscriptions_controller.rb
2
3class SubscriptionsController < ApplicationController
4 # ...
5 def index
6 @subscription = Subscription.by_user(user_id: current_user.id).first
7
8 # 移除 render plain: "..." 改為使用 View
9 # render plain: "#{@subscription.expired_in_days} 天後到期"
10 end
11 # ...
12end
這裡我們移除掉了 render plain: "..."
讓 Rails 改為使用 View 去呈現內容。
1<!-- app/views/subscriptions/index.html.erb -->
2<div><%= @subscription.expired_in_days %> 天後到期</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 %> 天</td>
15 </tr>
16 <% end %>
17 </tbody>
18</table>
我們增加了一個表格,會將 items
依序列出後印出「延展 30 天」這類字樣,這樣就可以通過新加入的測試條件。
現階段的實作我們已經有一些原本暫時不需要處理的實作可以重構,像是
#expired_in_days
應該是要由 Helper 來實現,但是現在跟聚合的資料綁定在一起,這表示我們需要做一些調整來改善這個狀況。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 前言 - Rails 開發實踐
- 將需求實現的準備 - Rails 開發實踐
- 獲取規格的技巧 - Rails 開發實踐
- 可以測試的規格 - Rails 開發實踐
- 快速通過測試的方法 - Rails 開發實踐
- 用測試完善規格 - Rails 開發實踐
- 資料跟資訊的差異 - Rails 開發實踐
- 用測試資料驗證邏輯 - Rails 開發實踐
- 預期外狀況的檢查 - Rails 開發實踐
- 預期外狀況的測試 - Rails 開發實踐
- 聚合多筆資料 - Rails 開發實踐
- 重構與修正邏輯 - Rails 開發實踐
- 加入聚合實體 - Rails 開發實踐
- 持久化資料 - Rails 開發實踐
- 實體與倉庫 - Rails 開發實踐
- 聚合與邊界 - Rails 開發實踐
- 使用案例與服務 - Rails 開發實踐
- 結語 - Rails 開發實踐