我們已經初步的完成可以給前端使用的 API 實作,然而在這個狀態下後端並沒有實際保存資料的能力,也有一些不好的實作方式(如:@@items
的 Class Variable)因此接下來我們要整合 SQLite 來作為持久化儲存的機制。
使用 ActiveModel
平常除了直接使用 Ruby on Rails 開發外,也有非常多 Ruby 專案會利用構成 Rails 的套件,其中 ActiveModel 就是一套能讓我們非常輕鬆建構資料與物件映射(ORM,Object Relation Mapping)的套件。
先修改 Gemfile
加入 ActiveModel 讓我們可以繼續後續的實作。
1# ...
2gem 'activemodel'
修改完畢後,運行 bundle install
安裝套件。
接著修改 app.rb
在 Bundle.require()
下方引用 ActiveModel 和 Model 目錄下的檔案。
1require 'bundler/setup'
2Bundler.require(:default, ENV['RACK_ENV'].to_sym)
3
4# 因為 active_model 跟 activemodel 沒有對應,Bundler 有可能偵測不到
5require 'active_model'
6Dir['models/*.rb'].sort.each { |path| require_relative path }
7
8# ...
商品模型
接下來,我們要用商品模型來管理原本寫死的作法,也讓我們可以根據情況測試對應的實作。修改 features/products.feature
加入商品資料的描述。
1#language:zh-TW
2功能: 商品
3 背景:
4 假設 這裡有一些商品
5 | id | name | price |
6 | 1 | Ruby 秘笈 | 100 |
7 | 2 | RSpec 秘笈 | 150 |
8# ...
繼續更新 features/step_definitions/common.rb
將建立商品資料的行為實作進去。
1# ...
2
3Given('這裡有一些商品') do |table|
4 table.hashes.each { |item| Product.create!(**item) }
5end
我們還需要修改 features/support/env.rb
在每次測試前重設商品資料,以免造成互相干擾。
1Before do
2 Product.reset
3end
接著加入 models/product.rb
實作一個以 ActiveModel 定義的商品模型。
1# frozen_string_literal: true
2
3# :nodoc:
4class Product
5 class << self
6 def items
7 @items ||= []
8 end
9
10 alias all items
11
12 def create!(**attributes)
13 items << new(**attributes)
14 end
15
16 def reset
17 @items = []
18 end
19 end
20
21 include ActiveModel::Model
22 include ActiveModel::Attributes
23 include ActiveModel::Serializers::JSON
24
25 attribute :id, :integer
26 attribute :name, :string
27 attribute :price, :integer
28end
上面利用 ActiveModel 定義了商品應該要具備 id
、name
、price
三個屬性,並且我們利用 class << self
在 Product 這個類別上定義了簡單的商品資訊紀錄行為,有了 ActiveModel 的加成,我們也可以用 Product.new(id: 123, name: 'Ruby 秘笈')
這樣的方式建立物件,就會比自己實作容易不少。
在這裡我們隱性的遵照 Ruby on Rails 習慣的做法,如果未來要轉換到同類型的框架,就可以省下許多修改的時間。
那麼,我們在 app.rb
中的 /api/products
就可以從寫死的版本修改為這個版本。
1# ...
2module Shop
3 # :nodoc:
4 class API < Grape::API
5 # ...
6 namespace 'api' do
7 get '/products' do
8 Product.all
9 end
10 # ...
11 end
12 end
13end
因為我們已經在 Product 中引用了 ActiveModel::Serializers::JSON
這個模組,因此 Grape 也能順利的呼叫 #to_json
轉換成正確的 JSON 內容回傳。
購物車模型
根商品一樣,在購物車的部分我們也可以建立一個購物車模型來保存狀態。繼續加入 models/cart.rb
來實作購物車的行為。
1class Cart
2 class << self
3 def find(_id)
4 @find ||= new
5 end
6
7 def reset
8 @find = nil
9 end
10 end
11
12 def items
13 @items ||= Hash.new { 0 }
14 end
15
16 def update_amount(id, count)
17 items[id] = [items[id] + count, 0].max
18 end
19end
目前購物車沒有自己的屬性,反而是有一個類似於 CartItem 的概念存在,因此可以先不用引用 ActiveModel 的模組來處理屬性。
另一方面,我們目前也沒有根據使用者區分購物車的需求,因此在 Cart.find(1)
的實作中,永遠回傳一個 Cart 的實例,這樣後續的邏輯就可以統一在購物車中進行。
在這邊我們用了 @items ||= Hash.new { 0 }
來初始化一個 Hash 並且讓預設值永遠是 0
這樣就不需要額外再做一次初始化,那麼在 #update_amount
方法,我們就可以直接用 items[id] + count
的方式計算更新後的數量。
為了避免負數的狀況出現,可以利用 [x, y].max
的方式取出大於等於 0
的數值。接下來將 app.rb
進行修改,改為使用 Cart 模型來處理購物車的內容。
1# ...
2
3module Shop
4 # :nodoc:
5 class API < Grape::API
6 # ...
7
8 namespace 'api' do
9 # ...
10
11 params do
12 requires :id, type: Integer, desc: 'Product ID'
13 optional :amount, type: Integer, default: 1
14 end
15 post '/cart' do
16 @cart = Cart.find(1)
17 @cart.update_amount(
18 params[:id],
19 params[:amount]
20 )
21 @cart.items
22 end
23 end
24 end
25end
因為我們最終要看到的是 { '1': 1 }
這樣的內容,剛好是 @cart.items
保存的資料,因此在呼叫完畢 #update_amount
後直接以 @cart.items
作為回傳。
在運行 Cucumber 測試之前,我們還需要把 features/support/env.rb
原本把 @@items
重設掉的實作,改為 Cart.reset
讓每次運行測試時可以還原到初始狀態。
1# ...
2Before do
3 Product.reset
4 Cart.reset
5end
最後,運行 bundle exec cucumber
就能通過測試,並且透過重構將原本的實作抽離出來以 Model 物件進行管理跟維護。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 同時完成測試與文件 - Cucumber 的文件測試法
- 基本語法:功能描述 - Cucumber 的文件測試法
- 基本語法:驗證行為 - Cucumber 的文件測試法
- 基本語法:步驟定義 - Cucumber 的文件測試法
- 基本語法:輔助設定 - Cucumber 的文件測試法
- 前端環境:Vite 與 Cucumber - Cucumber 的文件測試法
- 商品列表與加入購物車 - Cucumber 的文件測試法
- 重構與移出購物車 - Cucumber 的文件測試法
- 商品資料與總價 - Cucumber 的文件測試法
- 結帳與結果 - Cucumber 的文件測試法
- 整理前端實作 - Cucumber 的文件測試法
- 初始化後端專案 - Cucumber 的文件測試法
- 商品資料 API - Cucumber 的文件測試法
- 更新購物車 API - Cucumber 的文件測試法
- 加入資料模型 - Cucumber 的文件測試法
- 持久化保存 - Cucumber 的文件測試法
- 結帳處理 - Cucumber 的文件測試法
- 在 Rails 的前後端分離 - Cucumber 的文件測試法
- 匯入前端實作 - Cucumber 的文件測試法
- 重現後端實作 - Cucumber 的文件測試法
- 累積價值 - Cucumber 的文件測試法