重构 Rails 重构: 利用 Service 优化 Fat Model

zamia · 发布于 2014年10月22日 · 最后由 codemonkey 回复于 2016年11月01日 · 8968 次阅读
3214
本帖已被设为精华帖!

Rails重构: 利用Service优化Fat Model

给公司内部写的文章,分享出来,希望对大家有帮助。

本文主要说明什么时候需要重构fat model,以及通过一个简单的案例来讲解如何一步步重构复杂的model,把它变成扩展性良好的service。

源起

在这篇文章 7 Patterns to Refactor Fat ActiveRecord Models 中,其中提到了一点,利用Service重构 “Fat” Model。

原文中提到了几点需要重构成service的场景:

  1. 功能逻辑比较复杂
  2. 功能涉及到了多个model的时候
  3. 此功能会和外部系统交互
  4. 此功能并非底层model的核心责任,关联不大
  5. 此功能可能会有多种实现方式

上面的总结很好,但是也很模糊,比如到底什么样的功能算是复杂?涉及到多个model就应该优化成service吗?怎么样才叫做关联不大?

每个人的判断可能不太一样,对于一个team来讲,需要一个相对比较明确的约定来定义什么时候,你的业务逻辑需要重构层一个service了。目前大鱼的开发团队是这么简单约定的:

  • 当model class中出现跨model的‘写’行为的时候

为什么是这样的约定?

因为一般出现了跨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的写法

常见的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

这样的写法有两个问题:

  • 从当前的逻辑来看,所有的order_contact更新的时候都必须更新另外两个model,可是可能马上需求就要变化。这种利用callback的写法,当需求变化的时候再改动就会比较困难,这个时候负责新功能的工程师需要理清楚原有的思路,并且必须陷入到ActiveRecord类中去;
  • 例子中的UserContact类和UserProfile类,可能很快也会变化,这个时候直接在 order_contact类 中调用它们的 attribute=() 方法就显得很不合适了;至少这些类需要提供一个写接口,这样才能应对变化;

好,接下来就把上面两个缺点给重构掉:

重构一下,去掉回调

基本的策略是把 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

上面的代码利用类函数的方法把“写”代码移到了正确的类中,代码看起来清晰了一些,但是好像复杂性并没有降低,反而有点冗余了:

  • 在controller中原来是自动调用,现在需要写独立的代码,以后将来又有了新的类,不只是UserProfile和UserContact呢?就得在很多地方多添加一行,比如新的类是 UserInfo,那在每个controller里面都必须都写一行;
  • 静态函数里面其实隐含了一个需求,那就是更新常用联系人是根据 真实姓名 来更新的, 在这一行里面提现:
user_contact = user.user_contacts.by_real_name(real_name).first_or_initialize

其实这个就是典型的业务逻辑了,显然也不应该放藏的这么深,将来也很难去维护。

那么,有没有更好的办法呢?

Service出场,封装成service

利用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

经过上面的改动,有没有感觉代码更清晰呢?这些写有下面几个好处:

  1. 把更新用户信息这个逻辑抽离,service本身是可复用的,可以用在任何地方,包括task;console的调试等;
  2. service的接口很明确,就是根据order_contact更新用户的信息;如果以后有了新的需求,我们可以添加新的接口;如果原有的需求发生了变化,也可以修改目前的方法;都是很简单的。

总结

总结一下什么时候应该抽取 service:

  1. 当发生跨model的写的时候。这不是必然,但是可以认为是一个信号,表示你的业务逻辑开始变的复杂了。同时,当跨model的“写”都遵守了这个规则时,rails的model层就会变成一个真正的 DAL(Data Access Layer),不再是混合了数据逻辑和业务逻辑的 “Fat Model”;
  2. 一般来讲,callback 是要慎用的,特别是 callback 里面涉及到了调用其他 model 、修改其他 model 的情况,这个时候就可以考虑把相关的逻辑抽成 service 。

其他像文章最初提到的一些规则都比较模糊,需要经验丰富的工程师才能比较明确的判断,比如业务逻辑比较复杂、相对独立、将来可能会被升级成独立的模块的时候,这些需要一定的经验积累才比较容易判断。

service 的好处,基本上是抽象层独立的类之后的好处:

  1. 复用性比较好。因为是 ruby plain object,所以复用性上很简单,可以用在各种地方;
  2. 比较独立,可扩展性比较好。可以扩展 service ,给它添加新的方法、修改原有的行为均可;
  3. 可测试性也会较好。

抽取service 的本质是要把数据逻辑层和业务逻辑区别对待,让 model 层稍微轻一些;Rails 里面有 view logic、data logic、domain logic,把它们区别对待是最基本的,这样才能写出更清晰、可维护的大型应用来。

  • 当然,上面的代码还有可以优化的空间,比如把 email、cellphone、real_name 作为一个结构体在各个接口之间传递,不过不是本篇关注的重点,就暂时不写了。

大概就是这些了,以上,欢迎指正。

共收到 22 条回复
5
# -*- encoding: utf-8 -*-

module ActiveRecord
  module CustomerCache
    extend ActiveSupport::Concern

    included do |base|
      belongs_to_document :customer # 客人

      before_validation :set_customer_cache, if: :customer_cache? # 解决 belongs_to :customer 时 customer= 继承问题。
    end

    # 缓存客人信息
    def customer=(val)
      self.customer_id       = val.try(:id).try(:to_s) # 关联客人
      self.customer_username = val.try(:username) # 帐号
      self.customer_nickname = val.try(:nickname) # 昵称
      self.customer_name     = val.try(:name)    # 收货人
      self.customer_mobile   = val.try(:mobile)  # 手机号
      self.customer_address  = val.try(:address) # 收货地址
    end
    alias :set_customer_cache= :customer=

      private

        def set_customer_cache
          self.set_customer_cache = self.customer
        end

        def customer_cache?
          self.customer_id_changed? && self.respond_to?(:customer)
        end
  end
end

多处调用:

# -*- encoding: utf-8 -*-

class Order < ActiveRecord::Base
  include ::ActiveRecord::CustomerCache
  include ::ActiveRecord::StoreCache
  include ::ActiveRecord::Tracker
    extend Enumerize

  has_many :line_items

  serialize :carts, JSON
  after_initialize :set_default_carts

  has_many_documents :issues

  attr_accessible :order_no, :quantity, :price, :shipping_fee, :payment, :status, :created_on, :paid_on, :shipped_on, :signed_on, :current_user, :assigned_count, :assigned_at

  enumerize :status, in: { exception: -1, pending: 0, paid: 2, shipping: 3, shipped: 4, signed: 6, billed: 9, refund: 11, closed: 13, archived: 99 }, default: :pending, scope: true, predicates: { prefix: true }

  paginates_per 10

  default_scope -> { order('updated_at DESC, customer_id DESC') } 
  # .............
1107

:plus1:

例子感觉不太好,不过自己也想不到更好的了,create_order_contact可以自己定义成业务逻辑方法,这样就可以避免使用回调了,并且程序并不会变得冗余。 个人认为回调应该处理和业务逻辑无关的东西,比如刷新冗余字段、添加操作记录之类,否则考虑什么时候会触发回调本身就是灾难。

在模型里声明模型间关联就代表这些类有耦合了,增加Service层改变不了这点,但是如果一个业务涉及两个不耦合的模型或者涉及到外部系统,可以增加Service层,这样两个模型和Service单向依赖,结构更美观点。

话说,最近在实践把模型里的代码(声明、原子的业务逻辑)按作用拆分到不同的module当中,然后mixin进来,模型类本身仅包含类似create_order_contact的业务逻辑,如果业务逻辑众多其实还可以把业务逻辑按域分离到module当中

973

看到 service 就很 taobao style 哈哈,就是把schema和business分开吧,最好还是mixin.

314

不错的帖子, 我提些看法,首先refresh_by_order_contactrefresh_from_order_contact 到底是哪个呢?:) 我觉得 UserContact#update_by_order_contactUserProfile#update_by_order_contact 这两个方法可能需要放到类 UserInfoService 中去实现, 理由两点:

  1. UserContactUserProfile 两个模型更瘦了

  2. 根据你目前提供的场景,在 UserInfoService 之外单独调用UserContact#update_by_order_contact 或者 UserProfile#update_by_order_contact 的情况应该不被允许或者不存在。

我做的一些项目和楼主说的情况比较类似,但是有些地方用到了事务,这些事务都放到了 service 中,然后这些涉及到多个模型的更新必须在这些 service 中去实现。

11222

楼主和一楼的写法都有问题。

楼主的写法, 用model至少还能保证transaction。 你用了service连transaction都丢了, 得不偿失。 而且service调用太复杂, 本身就是为这个action服务的,又不准备复用, 全部写进去又何妨。

一楼的写法更有问题。 你不是处理掉了垃圾, 而是把垃圾分类放进了另外几个隐蔽的垃圾箱而且更容易腐败。 另外, 你自己的业务逻辑为什么要放在ActiveRecord module下面。

2324

感觉是java编程转过来的

96

虽然看不懂,但是我凭嗅觉感到7楼是对的!

3253

#7楼 @limkurn 需求天天大变,还重构个毛。233

188

除了 service 外,还可以细分出 decorator, presenter 之类的。

我之前带队的项目的数据:

+----------------------+-------+-------+---------+---------+-----+-------+
| Name                 | Lines |   LOC | Classes | Methods | M/C | LOC/M |
+----------------------+-------+-------+---------+---------+-----+-------+
| Decorators           |   530 |   441 |      18 |      85 |   4 |     3 |
| Finance              |  1593 |  1372 |      52 |     204 |   3 |     4 |
| Presenters           |  1608 |  1464 |      68 |      78 |   1 |    16 |
| Queries              |    32 |    27 |       1 |       5 |   5 |     3 |
| Representers         |  1435 |  1138 |       5 |     170 |  34 |     4 |
| Services             |  3225 |  2589 |      75 |     333 |   4 |     5 |
| Uploaders            |    54 |    12 |       1 |       3 |   3 |     2 |
| Validators           |   486 |   430 |      23 |      54 |   2 |     5 |
| Controllers          |  1612 |  1298 |      55 |     165 |   3 |     5 |
| Helpers              |   224 |   184 |       0 |      40 |   0 |     2 |
| Models               |  4057 |  2204 |      78 |     210 |   2 |     8 |
| Mailers              |    12 |    11 |       1 |       1 |   1 |     9 |
| Javascripts          |   789 |   590 |       2 |     176 |  88 |     1 |
| Libraries            |  1352 |  1157 |      42 |     132 |   3 |     6 |
| Controller specs     |  1135 |   918 |       0 |       2 |   0 |   457 |
| Decorator specs      |   640 |   502 |       0 |       0 |   0 |     0 |
| Finance specs        |  1298 |  1012 |       0 |       0 |   0 |     0 |
| Helper specs         |    44 |    36 |       0 |       0 |   0 |     0 |
| Lib specs            |    83 |    67 |       0 |       0 |   0 |     0 |
| Mailer specs         |    30 |    22 |       0 |       0 |   0 |     0 |
| Model specs          |  2847 |  1477 |       4 |       4 |   1 |   367 |
| Presenter specs      |   607 |   459 |       1 |       0 |   0 |     0 |
| Representer specs    |  1327 |  1030 |       0 |       0 |   0 |     0 |
| Routing specs        |    13 |    11 |       0 |       0 |   0 |     0 |
| Service specs        |  2528 |  1903 |       2 |       8 |   4 |   235 |
| Validator specs      |   691 |   527 |       0 |       2 |   0 |   261 |
| View specs           |   116 |    87 |       0 |       0 |   0 |     0 |
| Acceptance specs     |  2432 |  2274 |       0 |       0 |   0 |     0 |
| Javascript specs     |   630 |   433 |       0 |     173 |   0 |     0 |
+----------------------+-------+-------+---------+---------+-----+-------+
| Total                | 31430 | 23675 |     428 |    1845 |   4 |    10 |
+----------------------+-------+-------+---------+---------+-----+-------+
  Code LOC: 12917     Test LOC: 10758     Code to Test Ratio: 1:0.8

而且,即便是小项目,也是个好习惯。我两周前刚开始自己写的一个小产品的数据——

+----------------------+-------+-------+---------+---------+-----+-------+
| Name                 | Lines |   LOC | Classes | Methods | M/C | LOC/M |
+----------------------+-------+-------+---------+---------+-----+-------+
| Decorators           |     6 |     6 |       2 |       1 |   0 |     4 |
| Services             |   155 |    95 |       3 |      12 |   4 |     5 |
| Controllers          |   310 |   201 |       5 |      34 |   6 |     3 |
| Helpers              |     5 |     5 |       0 |       1 |   0 |     3 |
| Models               |   202 |    77 |       6 |       6 |   1 |    10 |
| Mailers              |     0 |     0 |       0 |       0 |   0 |     0 |
| Javascripts          |    68 |    45 |       0 |       8 |   0 |     3 |
| Libraries            |     0 |     0 |       0 |       0 |   0 |     0 |
| Controller specs     |   323 |   238 |       1 |       2 |   2 |   117 |
| Decorator specs      |    19 |    14 |       0 |       0 |   0 |     0 |
| Helper specs         |     9 |     7 |       0 |       0 |   0 |     0 |
| Model specs          |   217 |    85 |       0 |       0 |   0 |     0 |
| Service specs        |    82 |    63 |       0 |       0 |   0 |     0 |
| Acceptance specs     |   117 |   108 |       0 |       0 |   0 |     0 |
| Javascript specs     |    31 |     0 |       0 |       0 |   0 |     0 |
+----------------------+-------+-------+---------+---------+-----+-------+
| Total                |  1544 |   944 |      17 |      64 |   3 |    12 |
+----------------------+-------+-------+---------+---------+-----+-------+
  Code LOC: 429     Test LOC: 515     Code to Test Ratio: 1:1.2
967

赞同 @jasl 的这句话

个人认为回调应该处理和业务逻辑无关的东西,比如刷新冗余字段、添加操作记录之类,否则考虑什么时候会触发回调本身就是灾难。

15

#10楼 @fredwu 请问有没有那个示例项目包含这些的?谢谢。

654

#7楼 @limkurn

感觉这些东西都是细枝末节~~~~(需求天天大变,还重构个毛)

+1

96

#2楼 @jasl
不同意这个说法。‘’在模型里声明模型间关联就代表这些类有耦合了,增加Service层改变不了这点。但是如果一个业务涉及两个不耦合的模型或者涉及到外部系统,可以增加Service层,这样两个模型和Service单向依赖,结构更美观点“。原因是透过ActiveModel看本质,通过model关联还是不通过model关联的类,还是通过http接口关联的服务,从设计上来看没有任何本质区别,而ls提出不增加Service类的设计违背了单一职责原则。(写过基础服务的亲们这点体会应该会更深,因为基础服务打成库给业务开发用的时候也就是只剩下一个rpc类的方法名了,其实activemodel就是把数据库服务打包了阿,只是这些rpc类设计的都很优秀很稳定,因为我们一般没机会去处理相关的错误,感觉不到这点。)

案例分析如下: 首先,明确一个事情,两个class之间耦合的含义是”两个class之间通过双方约定的接口耦合(ruby没interface呐就是使用public方法了),两个class的维护者保持接口语义的不变的前提即可任意维护自己class的实现。“这是面向对象的基本思想。

然后,我们如果跳出ActiveModel来看,也不套用设计模式,我们只考虑一般的类设计。出现一个操作需要同时更新两个现在还无关联的class的时候,一般有两个选择: 选择一,将类O,P的实例o,p作为另一个类S的成员,由类S提供一个方法m,s.m调用p.mp,o.mo,m保证关联调用的一致性等,作为接口供调用,o.mo,p.mp只负责读写对象o,p内的数据,三个类各司其职,维护对外承诺的m,mo.mp三个接口的语义。 选择二,根据情况如果这两个类中会有一个起主导作用的,比如对订单操作的类P,应该有信息需要从订单类O获得,这种情况类P因为总是是订单流程中的附属操作而不是反过来,所以就选择让订单类多一个成员p,由订单类提供对外的接口m调用自身的方法o.mo,和成员p的方法p.mp。也很好,少了一个类,而且还可能让O中的方法少了几个入参。

然后我们再看,对应的ActiveModel来说,就是belongs_to,has_many提供了语法糖来定义类的成员关系,也就是ls说的“他们已经耦合了”,那么自然看到ls是第二种选择。到此,从类设计的角度,我们可以很容易看到,选择二的问题在于:

1.变化发生,逻辑复杂了一点,由两者耦合变为三者耦合时候,比如又有了需要在工单处理过程中,运营人员类U需要对操作类P做操作的时候,对第一个选择依然是在s.m中添加u.mu处理三者的关系,保持接口不变。对选择二,如果保持接口不变,则要依然认为p属于o,在o.m里添加u.um;那么既然是三者关系,认为p应该属于u,改变接口为u.m里调用p.mp,o.mo是否也可以呢;还是P本身变得重要了,需要p.m作为接口调用u.um,o.om。那么我们看到当一个设计让修改发生时的选择太多时候,这个设计本身就很可能有问题。

2.继续分析,可能亲们已经有异议了,此时依照选择二背后的思路更好的选择是P作为接口p.m调用o.om,u.um,顺着人家ls的思路,这确实也是一个很好的解决方法阿?而问题正是在此出现,这个更好的设计为何在一开始功能逻辑简单为“只有订单操作需要记录”的时候很容易理解为options是订单的附属,应该由订单来提供接口呢?这正是这个设计思路的根本问题所在,这个想在多者关系中总选择一个主导者有其来提供接口的思路本身是难以应对变化的思路,不是一个好思路。这也正是经典的原则和思路能被后人一而再的承认的原因。因为设计模式、设计原则思路也好,都是前人在编写大规模超大规模程序的时候,在超出人脑处理复杂度之外而总结的,当面对一两千行代码时候那真的是咋写都行真没啥大区别,而教这些理念的书籍又不可能贴上十万行代码然后告诉你“二货,打开vim,脑子一片空白了吧,得靠好好设计了吧。”,那么在有限的篇幅里举出的猪猫狗这些class做的例子很容易被觉得是多此一举,可有可无的。而有更多语法糖的语言、越来越多成熟的库、更面向具体领域的框架、也一直在飞速降低同样功能的程序的代码行数,更多优秀架构师在产生的同时同样的人力资源的价格越来越贵,因此越是架构师在更高的层面解耦系统解耦代码库后再给其他人分工,设计的好坏越来越难有更多的人体会。

闲话扯太多,不增加一个service不好的本质原因是,这种设计违背了单一职责原则,这个出自敏捷始书里的经典面向对象原则。通过增加类S,我们界定清楚了S的职责是且仅仅是组合o,p,u;O,P,U的职责也是且仅仅是维护对象自己。而不增加,无论如何取舍,这份逻辑找不到好的归处。

至于在开发过程中只要有跨model的写就必须有service,是严谨的设计对开发效率的折中,读的接口跨model也应该抽象,但不做简单的读接口的抽象的危害没有那么大,而当写操作出现的时候意味着会影响到其他读接口的语义能否在增加这个写操作之后依然正常工作,错误在这时候会加速扩散,这个时候就必须增加一个service类来进行更谨慎的设计防止错误扩散难以解决。另一个原因是,这个原则如此容易理解和执行和检查,KISS。

96

对ROR的开发没有太多的经验,所以没有太多发言权,但是整个例子看下来到最后有个疑问:系统中是不是会生成很多Service的实例?如: 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 每次调用都要UserInfoService.new 一个Service的实例。

8137

不明觉厉,但是我觉得这种处理必须还得事务保证,不然一个其中一个挂了,会产生一些莫名其妙的脏数据

9442

我意见是ruby就别当java用了,复杂业务直接java的好。另事务是个大问题。

96

楼主说的那篇文章我也看过,有个视频,是这个哥们参加rubyconf(还是Rails conf, 记不清了),也做过演讲。好像他做了code climate,用来测评代码质量,很多有名的gem也都在使用。他说的很多内容,也没能全理解,总之感觉就是把方法根据不同的功能,有规范地抽取出来,放到一些目录下,这样可以更多的做到每个类只负责自己的部分(“单一职责”?)。尝试在之前的项目里用form object重构过一些代码,试过之后确实给人一种神清气爽的感觉。包括像cancan的替代品pundit,它默认的做法就是创建一个policies的目录,把验证规则什么的都放在对应文件里,多少也是按照<7 patterns>的想法去做的。这种做法,我个人的感觉是,让Rails更加像一个ruby的工程,而不是一个大杂烩。 像Service什么的,看上去真心有种当初学java的感觉,定义很多接口,再给具体实现什么的。但我个人还是觉得,一个项目的话,先粗略设计一下,然后就上手做,想得太多太远,可能会造成设计过度。像这个ServiceObject, QueryObject什么的,可能会在某个时间点上再使用。

2454

非常同意楼上的看法。重构的方法很多种,一定要结合自己的业务逻辑来使用,将业务逻辑理解透彻。有些人用Service只是为了单纯的减少代码量,至要是有两个model中相同的代码就往service中硬塞进去,导致很难读懂service代码意图,原本面向对象编程被重构成了面向过程。

20518

#10楼 @fredwu 你用的什么查看项目数据?

F916e5

这是2年前的帖子了.我今天查找有关rails需不需要sevice层的帖子看到这个,作为以前写java程序,的确java开发都喜欢弄个service层,虽然现在也在学习rails.感觉楼主帖子,讲的有道理.适合自己业务和自己团队就好.在service层控制事务也可以啊,

@order = Order.create(params[:order])
@order_contact = @order.create_order_contact(params[:order_contact])

这个我觉得也放到service里,从楼主描述的业务需求,这三个(创建订单,更新常用联系人列表,更新用户唯一联系人信息)应该是统一的操作,假如有一个操作出现异常,这三个操作都用该回滚用,

ActiveRecord::Base.transaction do  end 

包裹起来 或者根据你们需求不需要保证一致性

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