服務類

控制器是我們應用程式的入口點。但是,它不是唯一可能的切入點。我想通過以下方式訪問我的邏輯:

  • 耙任務
  • 後臺工作
  • 控制檯
  • 測試

如果我將邏輯拋入控制器,則無法從所有這些位置訪問它。因此,讓我們嘗試“瘦控制器,胖模型”方法並將邏輯移動到模型中。但是哪一個?如果給定的邏輯涉及 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 部落格

程式碼氣候部落格