大話重構 之 防止“加個需求,到處改代碼”


上一篇《職責單一原則真的簡單嗎》中我們認識了 發散式變化 ,它是一個類包含多個維度的變化,職責不單一。本文討論的代碼壞味道是 散彈式修改 ,與 發散式變化 恰好相反,一個維度的變化涉及到多個類。

在商業項目開發過程中,經常會碰到“加個需求,到處改代碼”的情況,也就是 散彈式修改 ,典型后果是漏改某些地方,導致整個系統表現不一致。

要解決 散彈式修改 ,對重構/設計技能有較高要求。一如既往,一碼上個例子,與你分享其中需要理解的點點滴滴。

例子的背景

該例子來自於一個關於推薦和訂單的報表系統。

小伙伴們應該知道,報表系統說白了,就是以各種方式展示各種指標。簡單點,假設目前只有下面三個指標:

  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公眾號,點擊 核心技術,點擊 大話重構。

分類 大話重構

優雅程序員 原創 轉載請注明出處

圖片二維碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM