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

zamia · 2014年10月22日 · 最后由 codemonkey 回复于 2016年11月01日 · 18830 次阅读
本帖已被管理员设置为精华贴

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 作为一个结构体在各个接口之间传递,不过不是本篇关注的重点,就暂时不写了。

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

# -*- 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') } 
  # .............

:plus1:

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

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

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

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

不错的帖子,我提些看法,首先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 中去实现。

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

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

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

感觉是 java 编程转过来的

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

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

除了 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

赞同 @jasl 的这句话

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

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

#7 楼 @limkurn

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

+1

#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。

对 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 的实例。

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

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

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

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

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

这是 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 

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

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