给公司内部写的文章,分享出来,希望对大家有帮助。
本文主要说明什么时候需要重构 fat model,以及通过一个简单的案例来讲解如何一步步重构复杂的 model,把它变成扩展性良好的 service。
在这篇文章 7 Patterns to Refactor Fat ActiveRecord Models 中,其中提到了一点,利用 Service 重构“Fat”Model。
原文中提到了几点需要重构成 service 的场景:
上面的总结很好,但是也很模糊,比如到底什么样的功能算是复杂?涉及到多个 model 就应该优化成 service 吗?怎么样才叫做关联不大?
每个人的判断可能不太一样,对于一个 team 来讲,需要一个相对比较明确的约定来定义什么时候,你的业务逻辑需要重构层一个 service 了。目前大鱼的开发团队是这么简单约定的:
为什么是这样的约定?
因为一般出现了跨 model 的写的时候,说明你的业务逻辑比较复杂了,可以考虑封装成一个 service 来完成这件相对“独立”的功能;特别是如果 model 的回调中,出现了跨 model 的写,这种情况更应该避免,因为将来逻辑复杂时,很有可能回调的条件已不再满足了。
所以 service 的封装的粒度应该是比较明确的,那就是对于复杂的功能逻辑,如果同时又比较独立,将来有一定的可能性会扩展成一个 engine、甚至独立的组件(如 http api),那么显然是应该封装层 service 的,那么目前的一个“较好”的标准,就是如果 model 内部出现了跨 model 的“写”,应当考虑把这部分功能封装层 service,以便将来的扩展。
案例实现的是电商中常见的“常用联系人”的功能,下面是一个简化的需求说明:
OrderContact 订单联系人表:跟订单是 1:1 的
order_id: integer
real_name: string
cellphone: string
email: string
***: 其他字段忽略
UserContact 常用旅客表:跟 User 是 N:1 的
user_id: integer
real_name: string
cellphone: string
email: string
***: 其他字段忽略
UserProfile 用户基本信息表,跟 User 是 1:1 的
user_id: integer
real_name: string
cellphone: string
email: string
****: 其他字段忽略
如果是你来实现这个需求,你会怎么写?hoho,请继续看下去吧!
# 最基本的几个关联关系
class User
has_many :user_contacts
has_one :user_profile
end
class Order
has_one :order_contact
end
class UserContact
belongs_to :user
end
class OrderContact
belongs_to :order
end
class UserProfile
belongs_to :user
end
常见的 rails 的写法是,在 order_contact model 层添加 after_save 回调,分别去更新对应的 user_contact 和 user_profile 即可,写起来也会很快捷;
class OrderContact < ActiveRecord::Base
belongs_to :order
after_save :update_user_contact
after_save :update_user_profile
private
def update_user_contact
user_contact = order.user.user_contacts.by_real_name(real_name).first_or_initialize
user_contact.email = self.email
user_contact.cellphone = self.cellphone
user_contact.real_name = self.real_name
user_contact.save
end
def update_user_profile
user_profile = order.user.user_profile
user_profile.email = self.email
user_profile.cellphone = self.cellphone
user_profile.real_name = self.real_name
user_profile.save
end
end
class OrderController < ApplicationController
def create
# 创建订单的时候保存联系人信息
@order = Order.create(params[:order])
@order_contact = @order.create_order_contact(params[:order_contact])
end
end
这样的写法有两个问题:
好,接下来就把上面两个缺点给重构掉:
基本的策略是把 after_save 的回调,方法是在 controller 里面调用相关的方法了;然后我们要去掉直接在 model 里面去写另外一个 model 的逻辑,方法是让它提供相应的封装好的写接口;
提供封装好的写接口:
class OrderContact
# 删除原有的 after_save 以及相关的方法
end
class UserContact
def self.update_by_order_contact(user, order_contact)
user_contact = user.user_contacts.by_real_name(real_name).first_or_initialize
user_contact.real_name = order_contact.real_name
user_contact.cellphone = order_contact.cellphone
user_contact.email = order_contact.email
user_contact.save
end
end
class UserProfile
def self.update_by_order_contact(user, order_contact)
user_profile = user.profile
user_profile.real_name = order_contact.real_name
user_profile.cellphone = order_contact.cellphone
user_profile.email = order_contact.email
user_profile.save
end
end
然后在 controller 里面直接调用
class OrderController < ApplicationController
def create
# 创建订单的时候保存联系人信息
@order = Order.create(params[:order])
@order_contact = @order.create_order_contact(params[:order_contact])
# 调用写接口,更新user相关的信息
UserProfile.update_by_order_contact(@order.user, @order_contact)
UserContact.update_by_order_contact(@order.user, @order_contact)
end
end
上面的代码利用类函数的方法把“写”代码移到了正确的类中,代码看起来清晰了一些,但是好像复杂性并没有降低,反而有点冗余了:
user_contact = user.user_contacts.by_real_name(real_name).first_or_initialize
其实这个就是典型的业务逻辑了,显然也不应该放藏的这么深,将来也很难去维护。
那么,有没有更好的办法呢?
利用 service 的方法,我们把所有的业务逻辑抽离出来,把数据逻辑继续留在 model 层中;并且是把“更新用户信息”当做一个独立的小模块来实现,而现在这个 service 只提供一个接口,那就是根据 order_contact 来更新用户信息。
从这个角度看问题,我们创建 UserInfoService, 并且它的职责以及范围就很清楚了,继续往下改进:
model 层改成这个样子:
class UserContact < ActiveRecord::Base
def update_by_order_contact(order_contact)
self.real_name = order_contact.real_name
self.cellphone = order_contact.cellphone
self.email = order_contact.email
self.save
end
end
class UserProfile < ActiveRecord::Base
def update_by_order_contact(order_contact)
self.real_name = order_contact.real_name
self.cellphone = order_contact.cellphone
self.email = order_contact.email
self.save
end
end
新的 UserInfoService 只是一个简单的 ruby 类:
class UserInfoService
def initialize(user)
@user = user
end
def refresh_from_order_contact(order_contact)
# 更新常用联系人
user_contact = find_user_contact(order_contact.real_name)
user_contact.update_by_order_contact(order_contact)
# 更新用户个人信息
@user.profile.update_by_order_contact(order_contact)
end
private
def find_user_contact(real_name)
@user.user_contacts.by_real_name(real_name).first_or_initialize
end
end
新的控制器中代码的写法:
class OrderController < ApplicationController
def create
# 创建订单的时候保存联系人信息
@order = Order.create(params[:order])
@order_contact = @order.create_order_contact(params[:order_contact])
# 调用写接口,更新user相关的信息
UserInfoService.new(@order.user).refresh_by_order_contact(@order_contact)
end
end
经过上面的改动,有没有感觉代码更清晰呢?这些写有下面几个好处:
总结一下什么时候应该抽取 service:
其他像文章最初提到的一些规则都比较模糊,需要经验丰富的工程师才能比较明确的判断,比如业务逻辑比较复杂、相对独立、将来可能会被升级成独立的模块的时候,这些需要一定的经验积累才比较容易判断。
service 的好处,基本上是抽象层独立的类之后的好处:
抽取 service 的本质是要把数据逻辑层和业务逻辑区别对待,让 model 层稍微轻一些;Rails 里面有 view logic、data logic、domain logic,把它们区别对待是最基本的,这样才能写出更清晰、可维护的大型应用来。
大概就是这些了,以上,欢迎指正。