服务类
控制器是我们应用程序的入口点。但是,它不是唯一可能的切入点。我想通过以下方式访问我的逻辑:
- 耙任务
- 后台工作
- 控制台
- 测试
如果我将逻辑抛入控制器,则无法从所有这些位置访问它。因此,让我们尝试“瘦控制器,胖模型”方法并将逻辑移动到模型中。但是哪一个?如果给定的逻辑涉及 User
,Cart
和 Product
模型 - 它应该在哪里生活?
继承自 ActiveRecord::Base
的类已经承担了很多责任。它处理查询接口,关联和验证。如果你向模型添加更多代码,它将很快成为数百种公共方法无法维护的混乱。
服务只是一个常规的 Ruby 对象。它的类不必从任何特定的类继承。它的名字是动词短语,例如 CreateUserAccount
而不是 UserCreation
或 UserCreationService
。它位于 app / services 目录中。你必须自己创建这个目录,但 Rails 会为你自动加载类。
服务对象做一件事
服务对象(也称为方法对象)执行一个操作。它包含执行该操作的业务逻辑。这是一个例子:
# app/services/accept_invite.rb
class AcceptInvite
def self.call(invite, user)
invite.accept!(user)
UserMailer.invite_accepted(invite).deliver
end
end
我遵循的三个公约是:
服务属于 app/services directory
。我鼓励你将子目录用于业务逻辑繁重的域。例如:
- 文件
app/services/invite/accept.rb
将定义Invite::Accept
,而app/services/invite/create.rb
将定义Invite::Create
- 服务以动词开头(并且不以服务结束):
ApproveTransaction
,SendTestNewsletter
,ImportUsersFromCsv
- 服务响应
call
方法。我发现使用另一个动词使它有点多余:ApproveTransaction.approve()
读得不好。此外,call
方法是lambda
,procs
和方法对象的事实上的方法。
优点
服务对象显示我的应用程序的功能
我可以浏览服务目录,看看我的应用程序做了什么:ApproveTransaction
,CancelTransaction
,BlockAccount
,SendTransactionApprovalReminder
…
快速浏览一下服务对象,我知道涉及哪些业务逻辑。我不需要通过控制器,ActiveRecord
模型回调和观察者来理解批准交易涉及的内容。
清理模型和控制器
控制器将请求(参数,会话,cookie)转换为参数,将它们传递给服务,并根据服务响应重定向或呈现。
class InviteController < ApplicationController
def accept
invite = Invite.find_by_token!(params[:token])
if AcceptInvite.call(invite, current_user)
redirect_to invite.item, notice: "Welcome!"
else
redirect_to '/', alert: "Oopsy!"
end
end
end
模型仅处理关联,范围,验证和持久性。
class Invite < ActiveRecord::Base
def accept!(user, time=Time.now)
update_attributes!(
accepted_by_user_id: user.id,
accepted_at: time
)
end
end
这使得模型和控制器更容易测试和维护!
何时使用服务类
当某个操作符合以下一个或多个条件时,可以访问服务对象:
- 行动很复杂(例如,在会计期间结束时关闭账簿)
- 该操作涉及多个模型(例如,使用 Order,CreditCard 和 Customer 对象进行的电子商务购买)
- 该操作与外部服务交互(例如,发布到社交网络)
- 该行动不是基础模型的核心问题(例如,在一段时间后扫除过时的数据)。
- 有多种方法可以执行操作(例如,使用访问令牌或密码进行身份验证)。