大话重构 之 防止“加个需求,到处改代码”

上一篇《职责单一原则真的简单吗》中我们认识了 发散式变化 ,它是一个类包含多个维度的变化,职责不单一。本文讨论的代码坏味道是 散弹式修改 ,与 发散式变化 恰好相反,一个维度的变化涉及到多个类。

在商业项目开发过程中,经常会碰到“加个需求,到处改代码”的情况,也就是 散弹式修改 ,典型后果是漏改某些地方,导致整个系统表现不一致。

要解决 散弹式修改 ,对重构/设计技能有较高要求。一如既往,一码上个例子,与你分享其中需要理解的点点滴滴。

例子的背景

该例子来自于一个关于推荐和订单的报表系统。

小伙伴们应该知道,报表系统说白了,就是以各种方式展示各种指标。简单点,假设目前只有下面三个指标:

  1. UM(Unique Member),唯一身份的用户数,不区分线上线下
  2. Order,订单数,区分线上和线下
  3. Revenue,销售额,区分线上和线下

每个指标需要到数据仓库中去查询具体的值,然后在界面上展示出来。

原始代码

object QueryFieldBuilder {
  def build(fieldName: String): Array[String] = {
    if (fieldName.equalsIgnoreCase("order")
      || fieldName.equalsIgnoreCase("revenue"))
      Array("online", "instore").map(_ + fieldName.toLowerCase)
    else
      Array(fieldName.toLowerCase)
  }
}

查询指标值时,要分为两类处理。一是不需要区分线上和线下的指标,如UM,直接拿um作为查询字段即可;一是需要区分线上和线下的指标,如Order,需要转换成onlineorder和instoreorder。

object FiledValueFormatter {
  def format(filedName: String, value: String): String = {
    if (filedName.equalsIgnoreCase("revenue"))
      "$" + value
    else
      value
  }
}

展示指标值时,如果是钱,需要在前面加上美元符号\(。(如果工资前面直接加\)。。。)

加新指标Profit

Profit,利润额,区分线上和线下。。。

在原始代码中,为了加上新指标Profit,需要在QueryFiledBuilder和FiledValueFormatter两个主体中进行修改,额。。。大家都知道这样不好。

合并

通过移动方法的重构手法,把一个变化维度上的逻辑,移动到一个主体中。如果没有合适的主体作为方法的载体,则创建一个新主体。

object FieldContext {
  def buildQueryField(fieldName: String): Array[String] = {
    if (fieldName.equalsIgnoreCase("order")
      || fieldName.equalsIgnoreCase("revenue"))
      Array("online", "instore").map(_ + fieldName.toLowerCase)
    else
      Array(fieldName.toLowerCase)
  }

  def formatValue(filedName: String, value: String): String = {
    if (filedName.equalsIgnoreCase("revenue"))
      "$" + value
    else
      value
  }
}

新创建了FieldContext作为主体,承载两个方法。虽然一眼看过去,代码简单易懂,也没有 散弹式修改 的坏味道了。但是它职责单一吗?No

FieldContext包含了三个职责:

  1. 指标到查询字段的映射
  2. 指标值的格式化
  3. 添加指标

解决 散弹式修改 的过程中,通常会导致一点 发散式变化 ,那就又拆开呗。

合久必分

上面的三个职责耦合太紧,前两个职责完全依赖于第三个职责。

通过引入指标上的分类特性,来倒转依赖,从而分离上面的三个职责。

指标有两个分类特性,FieldChannel为OI表示需要区分线上线下,为Single表示不区分。ValueType为Money表示指标值是钱,为Normal表示不是钱。(之所以不用布尔值,是为了考虑以后的扩展)

case class Field(name: String, channel: FieldChannel, valueType: ValueType)

指标有了两个分类特性后,三个职责都可以依赖指标的分类特性,从而解耦。

object QueryFieldBuilder {
  def build(filedName: String): Array[String] = {
    val filed = FieldContext.getByName(filedName)
    val lowerCaseFiledName = filedName.toLowerCase

    if (filed.exists(_.channel.equals(OI)))
      Array("online", "instore").map(_ + lowerCaseFiledName)
    else
      Array(lowerCaseFiledName)
  }
}

QueryFieldBuilder依赖于指标的分类特性FieldChannel,承担职责“指标到查询字段的映射”。

object FieldValueFormatter {
  def format(filedName: String, value: String): String = {
    val filed = FieldContext.getByName(filedName)

    if (filed.exists(_.valueType.equals(Money)))
      "$" + value
    else
      value
  }
}

FieldValueFormatter依赖于指标的分类特性ValueType,承担职责“指标值的格式化”。

object FieldContext {
  private val fields = List(
    Field("UM", Single, Normal),
    Field("Order", OI, Normal),
    Field("Revenue", OI, Money),
    Field("Profit", OI, Money)
  )

  private val filedMap = fields
    .map(field => (field.name.toLowerCase, field))
    .toMap

  def getByName(name: String): Option[Field] = {
    filedMap.get(name)
  }
}

FieldContext通过给不同的指标配置合适的分类特性,来控制指标在查询字段映射和值格式化中的具体行为,完美承载职责“新增指标”。

新指标Profit的加入,只是FieldContext中的一行代码,一个配置而已。其实这是有学名的, 表驱动模式

小伙伴,你掌握了吗?

推荐

消除过长方法

消除过长类

消除重复代码

答粉丝问

你的参数列表像蚯蚓一样让人厌恶吗

职责单一原则真的简单吗

查看《大话重构》系列文章,请进入YoyaProgrammer公众号,点击 核心技术,点击 大话重构。

分类 大话重构

优雅程序员 原创 转载请注明出处

图片二维码

posted @ 2015-07-01 05:57  一码  阅读(3292)  评论(10编辑  收藏  举报