首先想说的是,Service Object 是在 Rails 里实践 SRP 的一种手段和模式,不仅仅是一个文件夹。
像 c/c++ 里,每个应用都会有一个入口,像下面这样:
#include <iostream>
// Many includes...
int main(int argc, char *argv[]) {
// Fetch your data.
// Ex. Input data = Input.readFromUser(argc, argv);
Application app = Application(data);
app.start();
// Cleanup logic...
return 0;
}
如果运行上面的应用,main 函数被调用,所有参数都被传递到argv
变量。
随着 c/c++ 程序代码量增长,没有人会把逻辑放到 main 函数里面,main 函数里只初始化一些常驻对象,然后调用 start 之类的方法去调用我们的业务逻辑。
Rails 的每个 Action 其实就相当于 c/c++ 里的 main 函数。
Rails 里,每个 Action 和 main 函数一样都是与外部交互的入口。Controller 和 Action 虽然表现为类和方法,但是不同 Action 相互之间是没有交互的。 另外,Controller 已经承担很多职责:
Service Object 封装了每一个业务流程。它负责组织应用领域模型(Model)之间的交互,并且不依赖于框架(Controller 层)。。你可以想象怎样的代码从 Sinatra 程序改成 Rails 会更容易,当然你不一定要这么做,我只是想解释一下什么是 不依赖框架层
。
引人 Service Object 之后,可以带来很多好处:
重构之前的 charge more controller:
class OrdersController
def charge_execute
@order = Order.find(params[:id])
@order.total = params[:order][:total].to_f
redirect_to :action => 'charge_more', :id => @order and return unless @order.total > 0
# 1. init beanstream payment gateway
gateway = ActiveMerchant::Billing::BeanstreamGateway.new(
:login => $BEAN_STREAM_ID,
:user => $BEAN_STREAM_LOGIN,
:password => $BEAN_STREAM_PASSWD
)
# 2. init creditcard, options or pair values
options = {
:order_id => @order.id,
:billing_address => {
:name => "#{@order.billing_first_name} #{@order.billing_last_name}",
:phone => @order.billing_phone,
:address1 => @order.billing_street_address,
:address2 => @order.billing_street_address_2,
:city => @order.billing_city,
:state => @order.get_state_code,
:country => @order.get_country_code,
:zip => @order.billing_zip
},
:email => @order.email,
:ref1 => "#{$DOMAIN_NAME} Order"
}
# 3. send payment info to gateway and deal with response
response = gateway.purchase(@order.total_in_cents, @order.get_creditcard, options)
if response.success?
credit_card = @order.credit_card
credit_card.transaction_id = credit_card.transaction_id + ',' + response.authorization
credit_card.save
flash[:notice] = "Extra money #{@order.total} has been charged"
else
flash[:notice] = "Error of processing charge: #{response.message}."
end
redirect_to :action => 'show', :id => @order and return
end
end
重构之后:
class OrdersController
def charge_execute
@order = Order.find(params[:id])
@amount = params[:order][:total].to_f
redirect_to :action => 'charge_more', :id => @order and return unless @amount > 0
charge_logic = OrderChargeLogic.new(@order, @amount * 100)
charge_logic.execute
if charge_logic.success?
flash[:notice] = "Extra money #{@amount} has been charged"
else
flash[:notice] = "Error of processing charge: #{charge_logic.message}."
end
redirect_to :action => 'show', :id => @order and return
end
end
Servies:
class OrderChargeLogic
attr_reader :order, :amount, :gateway, :response, :creditcard_options, :credit_card
def initialize(order, amount)
@order, @amount = order, amount
@credit_card = @order.credit_card
end
def success?
response.success?
end
def message
response.message
end
def execute
init_beanstream_payment_gateway
init_creditcard_options
send_payment_info_to_gateway
end
private
def init_beanstream_payment_gateway
@gateway = ActiveMerchant::Billing::BeanstreamGateway.new(
:login => $BEAN_STREAM_ID,
:user => $BEAN_STREAM_LOGIN,
:password => $BEAN_STREAM_PASSWD
)
end
def init_creditcard_options
@creditcard_options = {
:order_id => order.id,
:billing_address => {
:name => "#{@order.billing_first_name} #{@order.billing_last_name}",
:phone => order.billing_phone,
:address1 => order.billing_street_address,
:address2 => order.billing_street_address_2,
:city => order.billing_city,
:state => order.get_state_code,
:country => order.get_country_code,
:zip => order.billing_zip
},
:email => order.email,
:ref1 => "#{$DOMAIN_NAME} Order"
}
end
def send_payment_info_to_gateway
@response = gateway.purchase(amount, order.get_creditcard, creditcard_options)
if @response.success?
log_credit_card_transaction
end
end
def log_credit_card_transaction
credit_card.transaction_id = credit_card.transaction_id + ',' + @response.authorization
credit_card.save
end
end
Example 2: https://gist.github.com/hooopo/f6a031dac417323dfec6
引人 OrderChargeLogic 之后,收款这一个业务逻辑脱离了 Controller,可以在任何地方(rake task,model 等)复用。Controller 只负责调用 OrderChargeLogic,根据 OrderChargeLogic 返回的状态去设置提示信息并且渲染。
也就是说,当你的项目越来越复杂,Model 和 Database Table 不会完全一一对应了,同时也会有多个 Model 之间衍生出的业务逻辑,Service Object 就是用来处理这部分内容。这部分逻辑不属于 Controller,也不属于某一个 Model。
如果你想让复杂 Rails 项目也能 SRP,那么 Service Object 是一个值得尝试的手段。