上一篇《 职责单一原则真的简单吗 》中我们认识了 发散式变化 ,它是一个类包含多个维度的变化,职责不单一。本文讨论的代码坏味道是 散弹式修改 ,与 发散式变化 恰好相反,一个维度的变化涉及到多个类。
在商业项目开发过程中,经常会碰到“加个需求,到处改代码”的情况,也就是 散弹式修改 ,典型后果是漏改某些地方,导致整个系统表现不一致。
要解决 散弹式修改 ,对重构/设计技能有较高要求。一如既往,一码上个例子,与你分享其中需要理解的点点滴滴。
该例子来自于一个关于推荐和订单的报表系统。
小伙伴们应该知道,报表系统说白了,就是以各种方式展示各种指标。简单点,假设目前只有下面三个指标:
每个指标需要到数据仓库中去查询具体的值,然后在界面上展示出来。
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,需要在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包含了三个职责:
解决 散弹式修改 的过程中,通常会导致一点 发散式变化 ,那就又拆开呗。
上面的三个职责耦合太紧,前两个职责完全依赖于第三个职责。
通过引入指标上的分类特性,来倒转依赖,从而分离上面的三个职责。
指标有两个分类特性,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公众号,点击 核心技术,点击 大话重构。
分类 大话重构
优雅程序员 原创 转载请注明出处