具有主动模型模块的更智能的轨道服务
MVC 还不够!
我们熟悉 Rails 提供的 MVC(模型-视图-控制器)模式——模型映射到数据库表,并将数据包装在对象中;控制器接收数据请求并将数据提供给视图;视图呈现数据。一个常见的比喻是餐厅——模型是食物,控制器是接受订单并送上餐点的服务员,而视图是您用餐的精美餐桌。
MVC 是一种强大的 Web 应用设计模式。它帮助我们将代码放在合适的位置,构建架构良好的系统。但我们知道,MVC 并不能为大型 Web 应用的所有职责和功能提供框架。
例如,假设您有一个成功的在线商店的网络应用程序,您在其中销售一些有趣的东西,例如时间旅行设备。
当用户“结账”并完成购买时,您的应用需要完成相当多的工作。您需要:
- 识别正在进行购买的用户
- 识别并验证他们的付款方式
- 制定或完成购买
- 创建并向用户发送确认/收据电子邮件
更进一步,假设我们的购买创建实际上是通过 API 端点进行的:/api/purchases
。因此,我们购买处理的最终结果将涉及序列化一些数据。现在我们的职责列表还包括:
- 序列化已完成的购买数据。
这责任可真大啊!这么多业务逻辑该放哪儿呢?
控制器中没有,它唯一的职责是接收请求并获取数据。模型中也没有,它唯一的工作是从数据库包装数据。视图中当然也没有,它唯一的职责是将数据呈现给用户。
进入,服务对象。
服务对象来救援
服务对象是一个简单的 PORO 类(普通的 Ruby 对象),用于在 Rails 应用程序中包装复杂的业务逻辑。我们可以在控制器中调用服务,这样我们的PurchasesController
代码看起来就非常漂亮了:
# app/controllers/purchases_controller.rb
class PurchasesController < ApplicationController
def create
PurchaseHandler.execute(purchase_params, current_user)
end
end
我知道,很美,对吧?但如果我们的服务类需要变得更智能一些,不那么“平淡”一些,会发生什么呢?
思考一下上面我们处理购买的职责列表。该列表包括验证一些数据(我们是否找到了正确的用户?该用户的账户是否关联了有效的付款方式?)、序列化一些数据(通过我们的 API 端点返回)以及在购买完成后执行一些后处理(发送确认/发票电子邮件)。
有时 PORO 服务无法解决问题
虽然在 PORO 中处理所有这些职责完全有可能,但 Rails 也确实已经提供了一套强大的工具来处理这些确切的验证、序列化和后处理场景。这些听起来是不是很熟悉(提示一下,看看这篇文章的标题)……Active Model!
Active Model 为我们提供了用于验证、序列化和回调的工具,以便在调用特定方法后触发相应的回调。我们最熟悉 Active Record 工具箱,是通过继承模型代码来实现的ActiveRecord::Base
。你可能会猜想,这就是我们在服务中要做的事情PurchaseHandler
。但你错了。
我们不需要Active Record 提供的所有工具——我们不想将类的实例持久化到数据库中,而 Active Record 的许多模块都PurchaseHandler
处理这种交互。我们的服务类不是模型。它仍然是一种服务,其职责是封装执行购买操作的业务逻辑。
相反,我们将选择提供我们感兴趣的工具的特定活动模型模块,将这些模块包含在我们的服务类中,并利用它们提供的代码来满足我们的特定需求。
让我们开始并为我们的服务增添活力吧!
定义PurchaseHandler
服务
首先,我们先来规划一下服务类的基本结构。然后,我们再来考虑如何添加 Active Model 模块。
我们服务的 API 非常简单。它看起来像这样:
PurchaseHandler.execute(purchase_params, user_id)
看起来purchase_params
像这样:
{
products: [
{id: 1, name: "Tardis", quantity: 1},
{id: 2, name: "De Lorean", quantity: 2}
],
payment_method: {
type: "credit_card",
last_four_digits: "1111"
}
}
因此,我们的PurchaseHandler
类将公开以下方法:
# app/services
class PurchaseHandler
def self.execute(params, user_id)
# do the things
end
end
Active Model 模块将允许我们访问验证、序列化和回调钩子,所有这些钩子都可以在类的实例.execute
上使用。因此,我们的类方法需要初始化一个的实例PurchaseHandler
。
我喜欢让公共 API 保持非常简单——一个类方法——并使用该.tap
方法让这个面向公众的类方法保持非常干净。
#app/services
class PurchaseHandler
def self.execute(params, user_id)
self.new(params, user_id).tap(&:create_purchase)
end
end
这个#tap
方法非常简洁——它会将调用它的实例 yield 给一个块,并在块运行后返回调用它的实例。这意味着我们的.execute
方法将返回我们的handler
实例,然后我们可以在控制器中对其进行序列化(稍后会详细介绍)。
现在我们已经开始构建服务类,让我们开始引入验证处理程序所需的 Active Record 工具。
主动模型验证
我们的服务类需要什么样的验证?假设在处理或完成购买之前,我们需要验证以下内容:
- 给定的用户存在。
- 用户具有与给定付款方式匹配的有效付款方式。
- 所要采购的产品有所需数量的库存。
如果任何验证失败,我们希望将错误添加到处理程序实例的错误集合中。
该ActiveModel::Validations
模块将使我们能够访问 Active Model 验证方法,并且通过它自己包含的ActiveModel::Errors
模块,它将使我们能够访问.errors
attr_accessor 和对象。
我们将使用该#initialize
方法来设置执行购买所需的数据:
- 分配用户
- 指定购买方式(即用户的信用卡)
- 分配要采购的产品
该方法运行后,我们将对这些数据进行验证#initialize
。(这有点像回调?没错!)
让我们逐一构建验证。然后,我们将编写代码,借助 Active Model 的回调来调用验证。
首先,我们要验证我们是否能够找到具有给定 ID 的用户。
# app/services
class PurchaseHandler
include ActiveModel::Validations
validates :user, presence: true
def self.execute(params, user_id)
self.new(params, user_id).tap(&:create_purchase)
end
def initialize(params, user_id)
@user = User.find_by(user_id)
end
end
接下来,我们要验证是否能够找到与给定卡匹配的用户信用卡。注意:我们的信用卡查找代码比较简单。我们只使用了参数中包含的用户 ID 和卡号后四位数字。请注意,这只是一个简化的示例。
# app/services
class PurchaseHandler
include ActiveModel::Validations
validates :user, presence: true
validates :credit_card, presence: true
validates :products, presence: true
attr_reader :user,:payment_method, :product_params,
:products, :purchase
def self.execute(params, user_id)
self.new(params, user_id).tap(&:create_purchase)
end
def initialize(params, user_id)
@user = User.find_by(user_id)
@payment_method = CreditCard.find_by(
user_id: user_id,
last_four_cc: params[:last_four_cc]
)
end
end
最后我们来抓取需要购买的产品:
#app/services
class PurchaseHandler
include ActiveModel::Validations
validates :user, presence: true
validates :credit_card, presence: true
validates :products, presence: true
attr_reader :user, :credit_card, :product_params, :products, :purchase
def self.execute(params, user_id)
self.new(params, user_id).tap(&:create_purchase)
end
def initialize(params, user_id)
@user = User.find_by(user_id)
@payment_method = CreditCard.find_by(
user_id: user_id,
last_four_cc: params[:last_four_cc]
)
@product_params = params[:products]
@products = assign_products
end
def assign_products
Product.where(id: product_params.pluck(:id))
end
end
关于验证,我们需要考虑的最后一件事是:我们不仅需要知道产品是否存在于数据库中,还需要知道每种产品是否有足够的库存来满足订单。为此,我们将构建一个自定义验证器。
构建自定义验证器
我们的自定义验证器将被调用ProductQuantityValidator
,我们将通过以下行在我们的服务类中使用它:
#app/services
class PurchaseHandler
include ActiveModel::Validations
...
validates_with ProductQuantityValidator
我们将在app/validators/
# app/validators
class ProductQuantityValidator < ActiveModel::Validator
def validate(record)
record.product_params.each do |product_data|
if product_data[:quantity] > products.find_by(id: product_data[:id])[:quantity]
record.errors.add :base, "Not enough of product #{product_data[:id]} in stock."
end
end
end
工作原理如下:
当我们调用处理程序的验证功能时(我保证即将推出),该validates_with
方法会被调用。这会初始化自定义验证器,并使用最初validate
调用的处理程序实例作为参数来调用它。validates_with
我们的自定义验证器会查找给定记录(我们的处理程序实例)中每个选定产品的数量,如果该产品的库存不足,则会向记录(我们的处理程序)添加错误。
现在我们已经构建了验证,让我们编写代码来调用它们。
借助 Active Record 回调调用验证
在我们从数据库表继承并映射到数据库表的常规 Rails 模型中,当通过、或方法ActiveRecord::Base
保存记录时,Active Record 会调用我们的验证。.create
#save
#update
您可能还记得,我们的服务类没有映射到数据库表,也没有实现#save
方法。
我们可以通过调用公开的#valid?
方法来手动触发验证ActiveModel::Validations
。我们可以在方法中执行此操作#initialize
。
# app/services
class PurchaseHandler
include ActiveModel::Validations
validates :user, presence: true
validates :credit_card, presence: true
validates :products, presence: true
validates_with ProductQuantityValidator
...
def initialize(params, user_id)
@user = User.find_by(user_id)
@payment_method = CreditCard.find_by(
user_id: user_id,
last_four_cc: params[:last_four_cc]
)
@product_params = params[:products]
@products = assign_products
valid?
end
我们先想一想。#initialize
方法的作用是什么?Ruby 的#initialize
方法通过调用 自动调用,它的作用是构建类的实例。不仅要构建实例,还要验证它Klass.new
,这感觉有点超出了它的能力范围。我认为这违反了单一职责原则。#initialize
相反,我们希望在方法执行后#initialize
自动运行验证。
如果有一种方法可以让我们在一个对象生命周期的某个时间点自动触发某些方法……
开玩笑啦。当然有!Active Model 的回调函数就能让我们做到这一点。
定义自定义回调
首先,我们将把该ActiveModel::Callbacks
模块纳入我们的服务中。
# app/services
class PurchaseHandler
include ActiveModel::Validations
include ActiveModel::Callbacks
...
接下来,我们将告诉我们的类为我们的#initialize
方法启用回调。
# app/services
class PurchaseHandler
include ActiveModel::Validations
include ActiveModel::Callbacks
define_model_callbacks :initialize, only: [:after]
...
该#define_model_callbacks
方法定义了 Active Record 将附加回调的方法列表。
现在,我们可以像这样定义回调:
# app/services
class PurchaseHandler
include ActiveModel::Validations
include ActiveModel::Callbacks
define_model_callbacks :initialize, only: [:after]
after_initialize :valid?
...
在这里,我们告诉我们的类在我们的方法之后触发#valid?
(一种Active::Model::Validations
方法)#initialize
。
最后,为了真正调用我们的自定义回调,我们需要调用#run_callbacks
方法(由ActiveModel::Callbacks
),并将方法的内容包装在一个块中。
# app/services
class PurchaseHandler
include ActiveModel::Validations
include ActiveModel::Callbacks
define_model_callbacks :initialize, only: [:after]
after_initialize :valid?
...
def initialize(params, user_id)
run_callbacks do
@user = User.find_by(user_id)
@payment_method = CreditCard.find_by(
user_id: user_id,
last_four_cc: params[:last_four_cc]
)
@product_params = params[:products]
@products = assign_products
end
end
就是这样!
产生购买
现在我们已经正确验证了服务对象,可以开始实际执行购买操作了。为我们虚构的销售(非常真实的)时间旅行设备的在线商店生成虚假购买行为(很遗憾)并非本文的重点。因此,我们假设有一个额外的服务类,PurchaseGenerator
我们将在方法中调用它,#create_purchase
至于它的实现,我们暂且不谈。
#app/services
class PurchaseHandler
include ActiveModel::Validations
include ActiveModel::Callbacks
define_model_callbacks :initialize, only: [:after]
after_initialize :valid?
...
def create_purchase
PurchaseGenerator.generate(self)
end
这太简单了!我们编程太厉害了。
好的,现在我们可以创建购买,我们需要再次利用我们的自定义回调来处理后期处理 - 生成发票并发送电子邮件。
定义自定义after
回调
为什么这会在后处理中发生?好吧,我们可以把这个逻辑放在(这里没有编码)PurchaseGenerator
类中,或者甚至放在模型本身的一些辅助方法中Purchase
。这会产生我们不可接受的依赖程度。将购买生成与发票创建和电子邮件发送结合在一起意味着每次创建购买时,您都会生成发票和电子邮件。如果您是为测试用户或管理用户创建购买怎么办?如果您手动创建购买以向尊贵客户赠送免费赠品(医生不断购买 Tardis 替换部件)怎么办?当然可能会出现您不想同时实现这两种功能的情况。保持我们的代码简洁和模块化可以使其灵活和可重用,更不用说美观了。
既然我们已经确信这是一个好主意,那就让我们构建一个自定义回调,在处理发票和电子邮件确认的方法之后触发。我们#create_purchase
将定义两个私有辅助方法来执行此操作。我们还将使用该run_callbacks
方法包装内容,#create_purchase
以便回调能够触发。
#app/services
class PurchaseHandler
include ActiveModel::Validations
include ActiveModel::Callbacks
define_model_callbacks :initialize, only: [:after]
define_model_callbacks :create_purchase, only: [:after]
after_initialize :valid?
after_create_purchase :create_invoice
after_create_purchase :notify_user
...
def create_purchase
run_callbacks do
@purchase = PurchaseGenerator.generate(self)
end
end
private
def create_invoice
Invoice.create(@purchase)
end
def notify_user
UserPurchaseNotifier.send(@purchase)
end
序列化我们的服务对象
我们几乎完成了对服务对象的超级增强。最后但同样重要的是,我们希望能够序列ActiveModel::Serializer
化我们的对象,以便我们能够/api/purchases
使用简洁的 JSON 包来响应请求。
我们唯一需要添加到模型中的就是包含ActiveModel::Serialization
模块:
# app/services
class PurchaseHandler
include ActiveModel::Validations
include ActiveModel::Callbacks
include ActiveModel::Serialization
现在我们可以定义一个自定义序列化器,PurchaseHandlerSerializer
并像这样使用它:
class PurchasesController < ApplicationController
def create
handler = PurchaseHandler.execute(purchase_params, user)
render json: handler, serializer: PurchaseHandlerSerializer
end
end
我们的自定义序列化器很简单,它只会挑选出我们想要序列化的一些属性:
# app/serializers
class PurchaseHandlerSerializer < ActiveModel::Serializer
attributes :purchase, :products
end
就是这样!
我们的最终PurchaseHandler
服务看起来是这样的:
class PurchaseHandler
include ActiveModel::Validations
include ActiveModel::Callbacks
include ActiveModel::Serialization
define_model_callbacks :initialize, only: [:after]
define_model_callbacks :create_purchase, only: [:after]
after_initialize :valid?
after_create_purchase :create_invoice
after_create_purchase :notify_user
attr_reader :payment_method, :purchase, :products, :user, :product_params
def self.execute(params, user_id)
self.new(params, user_id).tap(&:create_purchase)
end
def initialize(params, user_id)
run_callbacks do
@user = User.find_by(user_id)
@payment_method = CreditCard.find_by(
user_id: user_id,
last_four_cc: params[:last_four_cc]
)
@product_params = params[:products]
@products = assign_products
end
end
def create_purchase
@purchase = PurchaseGenerator.generate(self)
end
private
def create_invoice
Invoice.create(purchase)
end
def notify_user
UserPurchaseNotifier.send(purchase)
end
结论
这是对一些更常用的模块的简要介绍,但我鼓励您深入研究 Active Model 工具箱。
ActiveRecord::Base
Active Model 是一款强大而灵活的工具。它的功能远不止我们在 Rails 应用中常见的“继承模型”那么简单。
MVC 是一种指导原则,并非一成不变。其核心思想是,我们不想让模型、视图或控制器与业务逻辑混杂在一起。相反,我们希望创建额外的对象来处理额外的职责。PORO 服务是一个很好的起点,但这并不总是足够的。为了增强我们的服务对象,我们可以借助 Active Model 的强大功能。
文章来源:https://dev.to/sophiedebenedetto/smarter-rails-services-with-active-model-modules-17o