新手问题 什么时候使用 Concerns,什么时候使用 Services?

QueXuQ · April 04, 2014 · Last by xiaoronglv replied at July 06, 2014 · 3782 hits

看了 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 里更合适,诸如此类的观点不一致的情况,会导致整个项目变得更凌乱,就针对这个问题,我没有想过有什么特别好的办法。 所以我还特别想了解,各个团队间应该怎么解决这样的问题比较好呢?

#5 楼 @JeskTop 听这么一说,确实不是一个简单的问题啊。

  1. 需要多个 model 团体协作时,用 Service Object

  2. 多个 model 需要 mixin 相同的逻辑时,用 concern

You need to Sign in before reply, if you don't have an account, please Sign up first.