域物件(不再是胖模型)
“Fat Model,Skinny Controller”是一個非常好的第一步,但是一旦你的程式碼庫開始增長它就不能很好地擴充套件。
讓我們考慮一下模型的單一責任 。模特的唯一責任是什麼?是否持有業務邏輯?是否保持與無響應相關的邏輯?
不,它的職責是處理持久層及其抽象。
業務邏輯以及任何與非響應相關的邏輯和非永續性相關的邏輯應該放在域物件中。
域物件是設計為在問題域中只有一個責任的類。讓你的類“ 尖叫他們的架構 ”解決他們解決的問題。
在實踐中,你應該努力去瘦瘦的模特,瘦的檢視和瘦的控制器。解決方案的體系結構不應受你選擇的框架的影響。
例如
假設你是一個通過 Stripe 向你的客戶收取固定 15%佣金的市場。如果你收取固定的 15%佣金,這意味著你的佣金會根據訂單的金額而變化,因為條紋收費為 2.9%+ 30¢。
你作為佣金收取的金額應為:amount*0.15 - (amount*0.029 + 0.30)
。
不要在模型中編寫此邏輯:
# app/models/order.rb
class Order < ActiveRecord::Base
SERVICE_COMMISSION = 0.15
STRIPE_PERCENTAGE_COMMISSION = 0.029
STRIPE_FIXED_COMMISSION = 0.30
...
def commission
amount*SERVICE_COMMISSION - stripe_commission
end
private
def stripe_commission
amount*STRIPE_PERCENTAGE_COMMISSION + STRIPE_FIXED_COMMISSION
end
end
只要你使用新的付款方式進行整合,你就無法在此模型中擴充套件此功能。
此外,只要你開始整合更多業務邏輯,你的 Order
物件就會開始失去凝聚力 。
首選域名物件,計算佣金完全從持久訂單的責任中抽象出來:
# app/models/order.rb
class Order < ActiveRecord::Base
...
# No reference to commission calculation
end
# lib/commission.rb
class Commission
SERVICE_COMMISSION = 0.15
def self.calculate(payment_method, model)
model.amount*SERVICE_COMMISSION - payment_commission(payment_method, model)
end
private
def self.payment_commission(payment_method, model)
# There are better ways to implement a static registry,
# this is only for illustration purposes.
Object.const_get("#{payment_method}Commission").calculate(model)
end
end
# lib/stripe_commission.rb
class StripeCommission
STRIPE_PERCENTAGE_COMMISSION = 0.029
STRIPE_FIXED_COMMISSION = 0.30
def self.calculate(model)
model.amount*STRIPE_PERCENTAGE_COMMISSION
+ STRIPE_PERCENTAGE_COMMISSION
end
end
# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
def create
@order = Order.new(order_params)
@order.commission = Commission.calculate("Stripe", @order)
...
end
end
使用域物件具有以下架構優勢:
- 它非常容易進行單元測試,因為不需要固定裝置或工廠來用邏輯例項化物件。
- 適用於接受訊息
amount
的所有內容。 - 保持每個域物件小,具有明確定義的職責,並具有更高的凝聚力。
- 通過新增而非修改, 可以輕鬆擴充套件新的付款方式。
- 停止在每個 Ruby on Rails 應用程式中擁有一個不斷增長的
User
物件的傾向。
我個人喜歡把域物件放在 lib
中。如果你這樣做,記得把它新增到 autoload_paths
:
# config/application.rb
config.autoload_paths << Rails.root.join('lib')
你可能還希望按照命令/查詢模式建立更加面向操作的域物件。在這種情況下,將這些物件放在 app/commands
中可能會更好,因為所有 app
子目錄都會自動新增到自動載入路徑中。