如果你是一名 Rails 开发人员,你可能见过(甚至写过)这样的代码。
class UserService
def self.create(params)
user = User.new(params)
if user.save
UserMailer.welcome_email(user).deliver_later
user
else
false
end
end
end
# 在控制器中
def create
@user = UserService.create(user_params)
if @user
redirect_to @user, notice: '用户已成功创建。'
else
render :new
end
end
全屏 退出全屏
这听起来熟悉吗?我们都有写过这样的服务对象,因为听说这是优秀Rails开发者应该做的事情。但让我问你一个问题:这个服务层实际上带来了什么价值?
我们听说服务层:
- 将业务逻辑从模型中分离出来
- 让代码更容易测试
- 保持控制器尽量简洁
- 遵循单一职责的准则
当你的服务只是多做一些打字工作时
许多 Rails 项目中的代码库充满了仅仅是将方法调用代理给 ActiveRecord 模型的服务类。让我们来看一个实际的例子:
class CommentService
# 用于创建评论的服务类,该方法用于为帖子创建评论
def self.create_for_post(post, user, content)
Comment.create(
post: post,
user: user,
content: content
)
end
end
全屏显示 退出全屏
到底得到了什么好处?除了多了一个多余的需要维护的文件和在调试时多了一层复杂的步骤外,什么也没有。这个服务实际上只是把参数传递给 Comment.create
,基本上没什么用。更糟糕的是,实际上我们失去了直接操作模型时的一些功能——我们再也无法利用 ActiveRecord 提供的丰富的验证错误处理和回调功能了。
当你确实需要服务层时
让我们明确一点:服务对象并不总是坏的。(实际上,我还写了一篇关于重新思考服务对象的文章——https://dev.to/alexander_shagov/ruby-on-rails-rethinking-service-objects-4l0b)
1. 调度复杂的操作流程
模块 订单管理
class 处理流程
include Dry::Transaction
步骤 :开始处理订单
步骤 :处理付款信息
步骤 :分配库存
步骤 :创建货运标签
步骤 :发送确认
步骤 :完成处理订单
私有:
def 开始处理订单(input)
订单 = input[:order]
订单.update!(状态: '正在处理')
Success(input)
rescue e
Failure([:订单处理失败, e.message])
end
def 处理付款信息(input)
订单 = input[:order]
支付 = 支付::Gateway.new.charge(
金额: 订单总金额,
token: 订单.payment_token
)
Success(input.merge(支付: 支付))
rescue e
Failure([:付款失败, e.message])
end
def 分配库存(input)
# ...
end
def 创建货运标签(input)
# ...
end
def 发送确认(input)
OrderMailer.confirmation(input[:order]).deliver_later
Success(input)
rescue e
Failure([:通知失败, e.message])
end
def 完成处理订单(input)
订单 = input[:order]
订单.update!(状态: '处理完成')
Success(input)
rescue e
Failure([:订单处理完成失败, e.message])
end
end
end
全屏模式 退出全屏
处理外部接口
模块 订阅
类 StripeWorkflow
包含 Dry::Transaction
步骤 :验证订阅有效性
步骤 :创建stripe用户
步骤 :创建stripe订阅项
步骤 :更新用户信息
私有
定义 验证订阅有效性(input)
合同 = 订阅::验证合约.new
结果 = 合同.call(input)
结果成功? ? 成功(input) : 失败([:验证失败, 结果的错误])
结束
定义 创建stripe用户(input)
# ... stripe 代码
结束
定义 创建stripe订阅项(input)
# ... stripe 代码
结束
定义 更新用户信息(input)
用户 = input[:user]
用户更新!(
stripe_customer_id: input[:customer].id,
stripe_subscription_id: input[:subscription].id,
subscription_status: input[:subscription].status
)
成功(input)
救援 => e
失败([:记录更新失败, e的错误信息])
结束
结束
结束
进入全屏,退出全屏
3. 复杂的业务条款
模块 贷款模块
类 应用程序流程
包含 干燥::交易
步骤 :验证申请
步骤 :检查信用评分
步骤 :评估债务比率
步骤 :计算风险评分
步骤 :确定批准
步骤 :处理结果
私有方法
定义 def 验证申请(输入)
合同 = 贷款::申请合同.new
结果 = 合同.call(输入)
结果成功? ? 成功(输入) : 失败([:验证失败了, 结果的错误])
结束
定义 检查信用评分(输入)
申请 = 输入[:申请]
如果 申请信用评分 < 600
失败([:信用评分过低, "信用评分低于最低要求"])
否则
成功(输入)
结束
结束
定义 评估债务比率(输入)
计算器 = 贷款::债务比率计算器.new(输入[:申请])
比率 = 计算器计算
如果 比率 > 0.43
失败([:债务比率过高, "债务收入比超过最大值"])
否则
成功(输入.合并(债务比率: 比率))
结束
结束
定义 计算风险评分(输入)
# ...(定义计算风险评分的实现)
结束
定义 确定批准(输入)
# ...(定义确定批准的实现)
结束
定义 处理结果(输入)
申请 = 输入[:申请]
如果 输入[:批准]
利率计算器 = 贷款::利率计算器.new(
申请: 申请,
风险评分: 输入[:风险评分]
)
申请更新!(
状态: '批准',
利率: 利率计算器计算,
批准日期: 当前时间()
)
贷款邮件.批准邮件(申请).延迟发送
否则
申请更新!(状态: '拒绝')
贷款邮件.拒绝邮件(申请).延迟发送
结束
返回成功(输入)
捕获异常 => e
失败([:处理失败了, e.message])
结束
结束
结束
全屏 / 退出全屏
上述示例代表了我认为有效的服务层使用场景——更准确地说,是业务流程。它们展示了抽象确实带来了实际的价值:复杂的流程、外部服务集成以及领域规则丰富的业务逻辑等。
不过,关键不只在于何时应用这些模式,还在于质疑我们对架构的默认思维。很多时候,我们选择服务对象仅仅是因为“这是标准做法”或因为有人认为“胖模型不好”。相反,我们应该:重新评估我们的架构决策,确保每一步都有明确的理由。
- 从简单开始 - 从模型和控制器入手
- 让复杂性引导抽象化 - 而不是相反
- 从流程和工作流的角度思考 - 而不是单纯依赖通用的“服务”
- 质疑现有的模式 - 只因为大家都在做,并不一定适合你自己的情况
更重要的是,如果你是大型团队的一员,首先要建立并达成一致的 统一 方法。代码库的一部分使用传统的服务对象,另一部分使用基于流程的工作方式,这很可能会带来更多问题而不是解决问题。架构决策应由团队共同做出,并需要有清晰的规范和文档作为支持。
记住,每一层抽象都是一个折衷。确保你获得的价值足以证明这种复杂度的付出是值得的。
共同学习,写下你的评论
评论加载中...
作者其他优质文章