域对象(不再是胖模型)
“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
子目录都会自动添加到自动加载路径中。