Rails 我这样使用 services,对吗?

QueXuQ · 2014年04月26日 · 最后由 QueXuQ 回复于 2014年08月05日 · 3165 次阅读

我有一个 Model Product,在创建的过程中会做各种操作,我使用 services,而不使用 Model 的 Concern,这对吗?

假设情况是这样的,我有 Model Order, Product, Store。没创建一个Product,库存Store就要发生变化,Order也要进行 Update。其实实际情况要更复杂一些,简单的描述就是: Controller

def create
  @product = Product.new(product_params)
  if @product.save_and_setting_all
   puts @product
  else
   puts @product.errors
  end
end

Model

def save_and_setting_all
  begin
    ActiveRecord::Base.transaction do
      @product.save!
      @product.order.update!(...)
      @product.store.update!(...)
    end
  rescue Exception => e
      false
  end
end

但是我觉得使用 services 更符合逻辑: Services

Class ProductAssistant
  attr_reader :product

  def initialize(params)
    @params = params
  end

  def save
    begin
      ActiveRecord::Base.transaction do
        @product.create!(@params)
        @product.order.update!(...)
        @product.store.update!(...)
      end
    rescue Exception => e
        false
    end
  end

  def errors
    @product.errors
  end
end

Controller 就改为:

def create
  @product = ProductAssistant.new(product_params)
  if @product.save
   puts @product
  else
   puts @product.errors
  end
end

我个人觉得这样会更加的优雅。不知道我这样做合适吗?

这里有一个这样的问题,目前我还不知道怎么解决,请可以帮我看看吗?

就是在使用Services的 Controller 里,@product.save了之后,我puts @product里的@product就不属于ProductModel 了,所以在 view 里的@product.name之类的就会出错了。不知道这个用什么办法赋值比较好呢?

我刚刚想到一个方法,在 Services 里:

def save
  begin
    ActiveRecord::Base.transaction do
      @product.create!(@params)
      @product.order.update!(...)
      @product.store.update!(...)
    end
    self = @product    #新增
  rescue Exception => e
      false
  end
end

结果不可以对 self 赋值,会报错。所以目前真想不到什么好办法了。

直接用 Service 代替 instance 就会出现你所说的问题。我比较喜欢把 instance 和 Service 分离,这样@product的 class 不变,其他也不受任何影响。

def create
   product = Product.new(params)
   creator = ProductCreator.new(current_user, product)
   @product = creator.create
   repsond_with @product
end

Service#create里面返回 product, 无论成功还是失败。

def create
  @product = CreateProductService.new(product_params).execute
  if @product.valid?
   puts @product
  else
   puts @product.errors
  end
end

@QueXuQ 你开篇提到的这个问题其实挺难的。

划分 Service 层还是使用 ActiveSupport::Concern,其实区别还是蛮大的。

使用 ActiveSupport::Concern 也就是把业务逻辑拆分成各种行为,而拆分出的行为以 module 的形式再 Mix-in 进 AR Class,并且你划分出去的 module 之间有依赖关系的话 ActiveSupport::Concern 会为你自动处理依赖,你只需要给你的 AR Class Mix-in 进你需要的 module 就好了不用去关心各个行为之间的依赖关系。但是如果你自己划分 Service 层,也就业务逻辑和数据持久化层分离了,Service 层不会被混入 AR Class。仅仅就是业务逻辑。

有没有必要在 Rails 上硬生生划出一个 Service Layer?很早以前曾经在社区里面有过激烈的争论

以 Bob 达叔为代表的一种观点认为应该把业务逻辑从数据模型层独立出去,不要将数据持久化和业务逻辑混在一起处理。DHH 觉得这种观点就像是手淫,有过比较偏激的言论 他说来说去就一种观点就是,认为 model 胖,业务复杂的问题完全可以通过 module 的混入机制来解决,他实际也是这么干的。

我个人觉得 ActiveSupport::Concern 机制完全没有任何问题,而且也更加 Ruby Way,体现了 Mix-in 的强大。至于为什么不用 ActiveSupport::Concern,有一篇博文讲的很详细,受到一些启发why i dont use activesupportconcern

还有一点,楼主你给 Service 起名字叫 ProductAssistant 这个是不对的,不管是 Service 还是 ActiveSupport::Concern 的 module 都应该是针对一种行为进行封装,@billy@ichord的代码他们起的名字叫ProductCreatorCreateProductService,我觉得这点很重要,面向单一功能的 Service 更有利于修改和 reuse,他不应该是个产品小助手,应该是一种具体的行为比如产品创建或是创建产品。

#3 楼 @ichord 原来是通过valid?来判断,非常赞。

#4 楼 @rockliu 我发觉还是用Service,觉得这样的维护更方便。

#1 楼 @billy 二楼的方案不错。

@QueXuQ 我的方法和二楼的没有本质的区别,但你如果有一些功能要依靠第三方 gem 的话,我的会比较方便一点。

比如说,我用 Pundit 做 authorization。Authorization 的逻辑既可以放在 Service 里面,也可以拿出来,我认为逻辑都是合理的。拿出来就可以利用现成的 method 和 controller context,比较方便一点,也比较省代码。但这样你就需要先有一个 product instance

def create
  product = Product.new(params[:rproduct])
  authorize post
  creator = ProductCreator.new(current_user, product)
  # ..
end

product.class 必须是 Prioduct 才能让 Pundit 正确判断。

#3 楼 @ichord 我发现单纯的学习 gitlab,还是不够的。因为我的例子里是有事务的:

begin
  ActiveRecord::Base.transaction do
    @product.create!(@params)
    @product.order.update!(...)
    @product.store.update!(...)
  end
  self = @product    #新增
rescue Exception => e
  false
end

所以如果是order出现错误的话,@product@product.valid?是依然为 true 的,而且还带不出 order 的错误信息。不知道你有没有什么好的建议呢?

#9 楼 @QueXuQ 那些都是如何组织代码的例子。不是具体的解决方法。

可以自己添加 errors 信息: @product.errors.add :base, e.message 然后换成检查 @product.persisted? 或者 @product.errors.empty? .

以上只是举个例子,具体要你自己考虑了。

因为我也不懂为什么新创建的 product 怎会有 product.order 然后可以 product.order.update . so.....

需要 登录 后方可回复, 如果你还没有账号请 注册新账号