Rails 重构: 利用 Service 优化 Fat Model

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值