服务类

控制器是我们应用程序的入口点。但是,它不是唯一可能的切入点。我想通过以下方式访问我的逻辑:

  • 耙任务
  • 后台工作
  • 控制台
  • 测试

如果我将逻辑抛入控制器,则无法从所有这些位置访问它。因此,让我们尝试“瘦控制器,胖模型”方法并将逻辑移动到模型中。但是哪一个?如果给定的逻辑涉及 UserCartProduct 模型 - 它应该在哪里生活?

继承自 ActiveRecord::Base 的类已经承担了很多责任。它处理查询接口,关联和验证。如果你向模型添加更多代码,它将很快成为数百种公共方法无法维护的混乱。

服务只是一个常规的 Ruby 对象。它的类不必从任何特定的类继承。它的名字是动词短语,例如 CreateUserAccount 而不是 UserCreationUserCreationService。它位于 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
  • 服务以动词开头(并且不以服务结束):ApproveTransactionSendTestNewsletterImportUsersFromCsv
  • 服务响应 call 方法。我发现使用另一个动词使它有点多余:ApproveTransaction.approve() 读得不好。此外,call 方法是 lambdaprocs 和方法对象的事实上的方法。

优点

服务对象显示我的应用程序的功能

我可以浏览服务目录,看看我的应用程序做了什么:ApproveTransactionCancelTransactionBlockAccountSendTransactionApprovalReminder

快速浏览一下服务对象,我知道涉及哪些业务逻辑。我不需要通过控制器,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 对象进行的电子商务购买)
  • 该操作与外部服务交互(例如,发布到社交网络)
  • 该行动不是基础模型的核心问题(例如,在一段时间后扫除过时的数据)。
  • 有多种方法可以执行操作(例如,使用访问令牌或密码进行身份验证)。

来源

Adam Niedzielski 博客

Brew House 博客

代码气候博客