重構與修正邏輯 - Rails 開發實踐
到目前為止,我們已經透過驗收測試保護了我們想實作的功能,然而有一些實作如果不儘快重構,很快就會變成難以維護的技術債,因此在提交部分程式碼後,我們需要盡快的對這些地方做處理。
區分命名與角色
我們在實作時,為了方便將 #expired_in_days
方法放在 Model 中,然而這個行為是「顯示」用的行為,更好的方式是以 Rails 的 View Helper 或者 Presenter 物件來處理,因此我們需要進行一個抽離的調整。
除此之外,因為加入了「訂閱紀錄」的機制,原本的 expired_at
欄位似乎也不太適合,改為 started_at
可能更能表達出訂閱機制的性質,因此我們還需要將這個欄位做出一些調整,來符合整個系統的脈絡(Context)
建立時間
我們希望將 expired_at
重新命名為 started_at
來符合新的商業邏輯,我們可以先著手修改 Model 的實作。
1class Subscription
2 # ...
3 attribute :started_at, :datetime, default: -> { Time.zone.now }
4 attribute :items, array: true, default: -> { [] }
5
6 def expired_in_days
7 (([0, started_at - Time.zone.now].max + items.sum.days) / 1.day).ceil
8 end
9
10 # ...
11end
如果試著跑測試,會發現因為我們改為 started_at
後,因為命名改變有許多地方都無法正常運作,因此可以先將命名修正。
1# app/services/create_subscription_service.rb
2class CreateSubscriptionService
3 # ...
4 def subscribe_for(amount:)
5 Subscription.new(user_id: @user_id, started_at: amount.days.from_now)
6 end
7end
1Given('會員 {word} 已經訂閱,並且在 {int} 天後到期') do |name, amount|
2 Subscription.create(user_id: @users[name], started_at: amount.days.from_now)
3end
4
5Given('會員 {word} 已經訂閱,並且在 {int} 天前到期') do |name, amount|
6 Subscription.create(user_id: @users[name], started_at: amount.days.ago)
7end
8
9Given('會員 {word} 有一些訂閱紀錄') do |name, table|
10 user_id = @users[name]
11
12 Subscription.create(user_id: user_id, started_at: 0.days.from_now)
13 subscription = Subscription.by_user(user_id: user_id).first
14 table.hashes.each do |row|
15 subscription.extend_with(amount: row['amount'].to_i)
16 end
17 subscription.save!
18end
不過這些調整也讓原本的 expired_at
的意義跟原本有點出入,因此我們還需要將 started_at
的時間修改為正確的樣子,搭配 #extend_with
方法來擴充正確的時間。
1class CreateSubscriptionService
2 # ...
3 def subscribe_for(amount:)
4 Subscription.new(user_id: @user_id).tap do |subscription|
5 subscription.extend_with(amount: amount)
6 end
7 end
8end
1Given('會員 {word} 已經訂閱,並且在 {int} 天後到期') do |name, amount|
2 Subscription.new(user_id: @users[name]).tap do |subscription|
3 subscription.extend_with(amount: amount)
4 end.save!
5end
在 CreateSubscriptionService
的部分,為了減少一次性過多的修改,我們先維持介面 #subscribe_for
的行為,利用 Ruby 的特性 #tap
暫時性的接續呼叫後續的處理來加入訂閱項目。
Rails 也預期可能會這樣使用,像是
User.create { |u| u.admin = ture }
之類的,在這邊因為我們沒有特別實作這樣的機制,因此利用#tap
來達成類似的方法。
抽離天數計算
我們將 #expired_at
轉換成 #started_at
後,就可以著手把計算過期日期的實作抽離出來,用 View Helper 來維護,避免 Subscription
中有額外的邏輯影響使用。
1# app/helpers/subscription_helper.rb
2
3module SubscriptionHelper
4 def expired_in_days(subscription:)
5 expired_at = subscription.started_at + subscription.items.sum.days
6 distance = [0, expired_at - Time.zone.now].max
7 (distance / 1.day).ceil
8 end
9end
原本的 #exired_in_days
方法也不太能明確的解釋意圖,在抽離的過程中也一起重構成比較明確的版本,讓脈絡更加清晰。
最後,清除掉 Model 上的實作,以及調整 View 之後,運行測試會發現測試恢復正常,這表示我們在沒有破壞產品的功能的狀態下,改善了原有的程式碼實作,以及調整了資料儲存的方式。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 前言 - Rails 開發實踐
- 將需求實現的準備 - Rails 開發實踐
- 獲取規格的技巧 - Rails 開發實踐
- 可以測試的規格 - Rails 開發實踐
- 快速通過測試的方法 - Rails 開發實踐
- 用測試完善規格 - Rails 開發實踐
- 資料跟資訊的差異 - Rails 開發實踐
- 用測試資料驗證邏輯 - Rails 開發實踐
- 預期外狀況的檢查 - Rails 開發實踐
- 預期外狀況的測試 - Rails 開發實踐
- 聚合多筆資料 - Rails 開發實踐
- 重構與修正邏輯 - Rails 開發實踐
- 加入聚合實體 - Rails 開發實踐
- 持久化資料 - Rails 開發實踐
- 實體與倉庫 - Rails 開發實踐
- 聚合與邊界 - Rails 開發實踐
- 使用案例與服務 - Rails 開發實踐
- 結語 - Rails 開發實踐