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

實體與倉庫 - Rails 開發實踐

這篇文章是 Rails 開發實踐 系列的一部分。

在我們實作訂閱功能的過程中,提到了像是 Entity(實體)還有 Repository(倉庫)等關鍵字,現在我們要來回顧一些這些使用的物件有怎樣的特性,在 Rails 中我們應該如何使用,才能避免預期外的問題。

實體

在 Rails 中我們所使用的 Model 就是 Entity,然而 ActiveRecord 同時也把這些 Model 物件賦予了 Repository 的角色,然而這並不代表他是混合的,這是因為 Ruby 語言的特性所造成的影響。

首先,對 Ruby 來說一切都被視為「物件」也因此一個類別(Class)也是一種物件,以 User 為例子,我們會有 User 類別物件和 #<User id=1> 的實例(Instance)兩種物件,前者是 Repository 的角色,因此會有 User.find(1) 的使用,而後者則是 Entity 的角色 user.admin?

因為 Entity 是用於維護「狀態」的物件,我們要避免寫入資料庫這類操作,這是由 Repository 負責的任務。基於這樣的原則,我們會發現 ActiveModel 會紀錄「狀態變化」這件事情,進而提供像是 user.changed?user.changes 的方法。

在大多數的狀況下,這類型的使用我們應該避免:

1class User < ApplicationRecord
2  # ...
3  def upgrade(role:)
4    update(role: role)
5  end
6end

這種類型的操作會讓「副作用(Side Effect)」產生,像是常見的 N + 1 查詢,或者在沒有注意到的狀況下不小心改動到使用者。

 1class User < ApplicationRecord
 2  # ...
 3  def upgrade(role:)
 4    self.role = role
 5  end
 6end
 7
 8# ...
 9users.each { |user| user.upgrade(role: :admin) }
10# 明確聲明「儲存」並且確保一起成功
11User.transaction { users.each(&:save!) }
12end

倉庫

Repository 是用來獲取資料的方式,實際上只要能夠將「資料」轉換成「資訊」就可以,簡單說就是取的資料後產生一個或多個實體,因此將他認知為一個「介面(Interface)」會更加適合,舉例來說除了 ActiveRecord 可以從資料庫獲取資料外,也可以用 ActiveResource 來抓到資料。

1class Person < ActiveResource::Base
2  self.site = "http://api.people.com:3000"
3end

因為 ActiveResource 和 ActiveRecord 提供了相同的介面,像是 .find.create 等方法,因此我們可以在拆分服務的狀況下,做出從 class Person < ActiveRecord 切換成 class Person < ActiveResource::Base 這樣子的切換,只要提供的方法還是相同,那麼就可以無痛轉移。

因此在使用上,比起單純使用 .find 這類基礎方法,我們可以更善用 scope 的機制來更清楚的描述我們的使用情境,舉例來說:

1class Subscription < ApplicationRecord
2  # ...
3  scope :unexpired, -> { where(expired_at: nil) }
4end

這樣就可以用 Subscription.unexpired 來表示「未過期的訂閱」在 ActiveResource 的版本下,可能就會像這樣實現。

1class Subscription < ActiveResource::Base
2  # ...
3
4  def self.unexipred
5    where(expired_at: nil)
6  end
7end

如此一來我們就可以在修改成外部服務時限縮重構的部分。

因為 Rails 比較少以這種方式拆分,因此大多數套件的介面跟 ActiveRecord 其實不太相容,在使用上還是要多評估,或者自己拆分出 Repository 而不要去依賴 ActiveModel 處理。