zoukankan      html  css  js  c++  java
  • 大话重构 之 防止“加个需求,到处改代码”

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

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

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

    例子的背景

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

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

    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公众号,点击 核心技术,点击 大话重构。

    分类 大话重构

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

    图片二维码

  • 相关阅读:
    有关UDP与TCP的一些疑问?
    UNP Ch 11, Name and Address Conversion
    C语言中的static关键字
    Typcical code to enable nonblocking I/O
    UNPv1_r3读书笔记: SCTP编程[转]
    用gcc链接重复定义的函数
    C语言编码风格 样例
    Chapter 3: File I/O
    getsockopt函数的使用
    开博客了
  • 原文地址:https://www.cnblogs.com/yoyaprogrammer/p/shortgun.html
Copyright © 2011-2022 走看看