看了 http://railscasts.com/episodes/398-service-objects 后,我心中有一个疑问,就是什么时候使用 Concerns,什么时候使用 Services?
我觉得他们两者的功能似乎都是差不多的,不知道大家是怎么组织的呢?还是只使用一个就好了?
concern 和 service 其实本质上是不一样的 concern 的重点是加强版的 mix-in 方便你组织不同模块代码 解耦的代码看起来会非常清爽 services 更多偏思想 一般是把跨 model 的调用写成 services 因为跨 model 的调用很有可能是公共的业务 实现的方式多种多样 我个人觉得这里如果用 concern 不过是能用 concern 实现罢了
简单来说就是,concerns
就是将部分简单的功能抽出来,然后多个模型共用,又或者有时候仅仅是模型的代码太多,将其相关逻辑的代码放到 concerns
里;
而 service
就是你只要交给它必要的数据,它就帮你把一件事情从头到尾全包了。
举两个简单的例子
module Orderable
extend ActiveSupport::Concern
module ClassMethods
def paginate_by_timestamp before_ts, after_ts, column_name = nil
column_name ||= "created_at"
column_name = "#{ table_name }.#{ column_name }" unless column_name.to_s.include?(".")
all.tap do |_scope|
_scope.where! ["#{column_name} < ?", before_ts] if before_ts
_scope.where! ["#{column_name} > ?", after_ts] if after_ts
end
end
end
end
class RegisterUserService
def initialize name, password
# ....
end
def go
# 创建用户
# 初始化创建一个用户后需要的各种基础数据
end
end
类比数据库,我是这么理解的:
Services 相当于 存储过程,属于业务功能组合,颗粒大,针对性强 Conerns 相当于 自定义函数,属于业务特性定义,颗粒小,通用性强
有时候想想,Active Record 就是把 数据库中的表关系,视图,存储过程,自定义函数,字段属性约束等,都挪到应用服务器这一端来
我觉得这是一个很复杂的问题,使用 Conerns 就这个意思:
为什么呢?我避免将一个大的 ActiveRecord 类里面的一部分方法放到某个关联类或者模块里面,然后将它们混入。我有一次听到这样的说法:
Any application with an app/concerns directory is concerning. 我同意,组合优于继承。但是,像这样使用混入就像是将混乱放到 6 个不同的抽屉然后关上它。确实,它表面上看去干净多了。但是垃圾抽屉的做法实际上使得它难以识别并且难以分解和提取业务模型。
使用 Conerns 就是把一个大的 Model,抽出来,然后放入 Conerns,或者是把 Model 里的方法分类的放入不同的 Conerns 里,例如这样:
class User < ActiveRecord::Base
...
#Virtual Attributes
def full_name
[first_name, last_name].join(' ')
end
def full_name=(name)
split = name.split(' ', 2)
self.first_name = split.first
self.last_name = split.last
end
...
end
上面是一个 User 里的一个Virtual Attributes
,我可以使用 Conerns,把Virtual Attributes
挪出去。添加一个virtual_attributes.rb
,到 Conerns 里:
class User
module VirtualAttributes
extend ActiveSupport::Concern
def full_name
[first_name, last_name].join(' ')
end
def full_name=(name)
split = name.split(' ', 2)
self.first_name = split.first
self.last_name = split.last
end
end
end
class User < ActiveRecord::Base
include VirtualAttributes
...
end
这样 Model 就瘦了一圈,当我要找 User 的 VirtualAttributes 时,到这个 Conerns 里找就可以了。一下方便了很多,也简洁了。Conerns 是非常通用的,所以我主要想讲一下我对 Services 用法的理解。
把刚刚的 VirtualAttributes 里放到 Services 里,看怎么样?
class UserVirtualAttributes
def initialize(user)
@user = user
end
def full_name
[@user.first_name, @user.last_name].join(' ')
end
def full_name=(name)
# 这个方法比较复杂,所以先不写
end
end
调用:
@user = User.find(params[:id])
@user_full_name = UserVirtualAttributes.new(@user).full_name
这个看起来貌似没有使用 Conerns 那么酷哦!当然,这种情况,就不适合使用 Services。那什么时候适合使用 Services?
其实 Services 是一个面向对象的思想,Rails 把所有东西都放到 MVC 里,所以当我第一次接触 Rails 的时候,顿时觉得和我认识的面向对象有点不太一样。我说说我对面向对象的理解,举个人骑自行车
的简单例子来说说我理解的面向对象。
如果我要做一个人骑自行车
的程序,至少要三个类,分别是:
人(手、腿等),骑的动作(脚踩一次脚踏板 => 自行车运动范围),自行车(轮,脚踏板等)
class People
...
def right_left
...
end
end
class Bike
...
def right_pedal
...
end
end
class RideBike
...
def ride
@people = People.new
@bike = Bike.new
...
end
end
大概意思这样子,感觉有点瞎扯,代码就不详细写下去了。主要要表达的意思就是,面向对象的思维就是把事物更合理的抽象出来的思想。
所以上面的 full_name 属于 User 的属性,应该放倒 User 里,那是不是我们现在 User 里的所有方法都是正确的呢?如果从面向对象的思想来说,我们常用的密码校验,密码加密等事情,就是不应该放在 User 里的,而应该抽象出来放到Services
中,如7种重构膨胀 ActiveRecord 模型的方法
说的一个例子:
class UserAuthenticator
def initialize(user)
@user = user
end
def authenticate(unencrypted_password)
return false unless @user
if BCrypt::Password.new(@user.password_digest) == unencrypted_password
@user
else
false
end
end
end
调用方法:
UserAuthenticator.new(user).authenticate(params[:password])
而大部分教材和我们管用的做法是:
class User < ActiveRecord::Base
...
def authenticate(unencrypted_password)
if BCrypt::Password.new(password_digest) == unencrypted_password
self
else
false
end
end
...
end
上面的案例我觉得是一个可以使用 Services 的情况。因为我觉得 UserAuthenticator 不属于 User 的时期。把 UserAuthenticator 放在 User 里,感觉就像:要判断一个人是否生病,还得用自己的方法
。而我觉得正确的表达是:使用特定的Services,来检查这个人是否生病了
。
我的观点是 Services 这个问题是非常复杂的,初建项目可以不需要考虑这么深层的问题。
我觉得 Services 最难使用的是如何保证团队里的思想一致性,情况就像如果我使用上面的UserAuthenticator
,而团队里的人员觉得放在 User 里更合适,诸如此类的观点不一致的情况,会导致整个项目变得更凌乱,就针对这个问题,我没有想过有什么特别好的办法。
所以我还特别想了解,各个团队间应该怎么解决这样的问题比较好呢?