識別代碼中的壞味道(二)

01.png

在上一篇文章中,介紹了通過名字就能理解的 8 個壞味道,感興趣可以查看識別代碼中的壞味道(一)。本篇文章將識別代碼中的另外 10 個代碼壞味道:10個晦澀但是通過簡單的即可識別的壞味道。

02.png

如上圖,這 10 個代碼壞味道是:

  1. 發(fā)散式變化
  2. 霰彈式修改
  3. 依戀情結(jié)
  4. 數(shù)據(jù)泥球
  5. 基本類型偏執(zhí)
  6. 平行繼承體系
  7. 冗贅類
  8. 過度耦合信息鏈
  9. 異曲同工的類
  10. 純數(shù)據(jù)類

01 發(fā)散式變化

簡而言之就是一個類總是因為不同類型的原因發(fā)生變化。例如:需要修改數(shù)據(jù)源時要修改該類,需要修改緩存時還需要修改這個類,甚至當(dāng)修改某個策略的計算公式時還會牽連到這個類。這種總是/經(jīng)常因為不同類型原因?qū)е乱粋€類發(fā)生變化的代碼就是指的發(fā)散式變化。

為什么發(fā)散式變化是代碼壞味道?

由于總是不同的原因?qū)е乱粋€類發(fā)生變化,意味著一個類中存在多種類型的行為(例如即操作訂單,又操作合同,還操作零件信息等),大而全的類會導(dǎo)致下面兩方面的問題:

  1. 降低了代碼可讀性,存在不同上下問題的切換;
  2. 很可能導(dǎo)致無法快速響應(yīng)變化。大而復(fù)雜類,在修改和維護(hù)的時候,并不容易做出決策,同時單個原因的修改很可能導(dǎo)致一個原因修改導(dǎo)致和非相關(guān)的業(yè)務(wù)代碼發(fā)生變動。
  3. 隨著代碼的增加,代碼的復(fù)雜性肯定是增加的,而發(fā)散式變化如果不被關(guān)注,很容易導(dǎo)致后續(xù)代碼修改時類變成難以修改的大泥球。

發(fā)散式變化很容易導(dǎo)致另外一個壞味道出現(xiàn),就是“過大的類”。

如何解決發(fā)散式變化這種壞味道?

單一職責(zé)原則可以用來解決發(fā)散式變化、過大的類的壞味道的指導(dǎo)原則:一個類只有一個引起其變化的原因。既然由于一個類存在過類行為,可以通過 Extract Class 來將不同的方法提煉到不同職責(zé)的類中。

發(fā)散式變化雖然很簡單,但是卻是很容易遇到的一種壞味道。因為剛開始添加的代碼的很可能體會不到一個存在多類行為的壞處。只有當(dāng)類發(fā)生變化或者修改的時候才會逐漸這種大而全的實現(xiàn)的缺點(diǎn)。

02 霰彈式修改

當(dāng)一個類進(jìn)行了修改會導(dǎo)致很多其他類也需要相應(yīng)進(jìn)行修改,我們稱為“霰彈式修改”。

為什么霰彈式修改是一種壞味道?

  1. 當(dāng)出現(xiàn)霰彈式修改的時候,容易造成修改上的遺漏,因此需要多次編譯、運(yùn)行測試、測試功能才有可能完全修改,雖然有的問題編譯的時候就可以發(fā)現(xiàn)已經(jīng)很快了,但是反復(fù)的編譯本來也是不斷花費(fèi)時間的,久而久之也是一種重復(fù)低效的。
  2. 不難發(fā)現(xiàn)一個類的變化導(dǎo)致其他類相應(yīng)的變化,這是一種強(qiáng)耦合的表現(xiàn)。

如何解決霰彈式修改這種壞味道?

既然霰彈式修改是一種耦合性的表現(xiàn),我們可以將相關(guān)的代碼通過 Move Field (移動屬性)和 Move Method (移動方法)兩種重構(gòu)手段將代碼移動到一個類中。這樣做的好處是讓變化的內(nèi)容聚集到了,有助于簡化后續(xù)的修改。

如果因為上面的操作類中添加了某些方法導(dǎo)致一個類有了多個職責(zé),那么可以在進(jìn)一步通過 Extract Method(提煉函數(shù))來拆分職責(zé)。

也可以創(chuàng)建代理類或者方法重載來來解決特定的霰彈式修改導(dǎo)致的問題。

03 平行繼承體系(Parallel Inheritance Hierarchies)

平行繼承體系指:當(dāng)一個類增加 1 個子類的時候,另外一個類也需要增加被迫增加一個子類。

例如:

03.png

當(dāng)添加 XXXVIPTaskService 的時候就會需要新增出新的 XXXVIPScoreService 。

為什么平行繼承體系是一種代碼壞味道?

  1. 顯而易見雖然沒有直接關(guān)聯(lián),但是兩者是同時產(chǎn)生并存的,但是兩者的關(guān)聯(lián)性并不顯性的呈現(xiàn),而是在 GradeService 中才體現(xiàn)出來。

  2. 這樣的實現(xiàn)容易導(dǎo)致在 GradeService 中 Switch 語句的產(chǎn)生,switch 語句本身就是一種重復(fù)的體現(xiàn)。關(guān)于Switch 語句的問題可以參考:識別代碼中的壞味道(一)

如何解決平行繼承體系這種代碼壞味道?

圍繞上面說的原因可以做出如下兩步重構(gòu):

  1. 建立直接引用。即 SVIPTaskService 直接引用 SVIPScoreService。
  2. 參考《Java有限狀態(tài)機(jī)的4種實現(xiàn)對比》 消除繼承體系,這里過程可以使用Move Field 和 Move Method 等重構(gòu)手法。

通過上面的重構(gòu),隱形的關(guān)聯(lián)變成直接引用。另外避免了 Switch 語句的問題。

04 依戀情結(jié)

剛開始接觸代碼中的壞味道時,乍一看你可能會覺得有些費(fèi)解。其實它描述的問題卻是很簡單的,就是:一個類多次調(diào)用另外一個類的方法來獲取最終的結(jié)果。如下

public class OrderService {

        public List<Order> findAllOrders() {
                ...
        }
        
        public Order findLatestOrder(List<Order> orders) {
                ...
        }
        
        public Order addProduct(Order order, Product product) {
                ...
        }

}

public class CartService {
    ...
      
    public void addProduct(Product product) {
        ...
        List<Orders> orders = orderService.findAllOrders();
        Order order = orderService.findLatestOrder(orders);
        order = orderService.addProduct(product);
        ...
    }
}

再是不用考慮上面這段的代碼業(yè)務(wù)上的合理性。代碼中 CartService 中多次調(diào)用 OrderService 的方法,其目的就是執(zhí)行最后的 addProduct() 方法,這就是一種依戀情結(jié)的代碼。

為什么依戀情結(jié)是代碼壞味道?

  1. 仔細(xì)觀察 CartService.addProduct() 方法不難發(fā)現(xiàn)那三行的代碼的意圖就是將 product 添加到最新的 order 中,如何實現(xiàn)將 product 添加到 product 這個目的,上面帶代碼顯然展示了一種策略的具體實現(xiàn)。顯然這種實現(xiàn)使得方法的職責(zé)不再單一。
  2. 另外一個問題是,當(dāng) OrderService中的 findAllOrders()、findLatestOrder()、addProduct() 方法因為需求發(fā)生變動的時候,都有可能會牽連到 CartService 中的代碼發(fā)生變化。因此上面中代碼通過強(qiáng)耦合性雖然實現(xiàn)了功能,但是應(yīng)對變化的能力也隨之降低。代碼是不斷演進(jìn)的,忽略了這種壞味道,會導(dǎo)致后續(xù)變化付出相應(yīng)的代價。

如何解決依戀情結(jié)這種代碼壞味道?

如果你看過上一篇內(nèi)容或者看過上面前兩個壞味道,那么應(yīng)該也有一些思路了,如果一類在一個方法中多次依賴另外一個類,我們可以立即為有可能是職責(zé)沒有劃分劃分明確的原因,可以通過一下手段進(jìn)行重構(gòu):

  1. 將多次產(chǎn)生調(diào)用的幾行代碼使用 Extract Method(提煉函數(shù))提煉為一個新的函數(shù),并通過名稱來解釋這幾個行代碼所要表達(dá)的意思。
  2. 接下來可以使用 Move Method (搬移函數(shù))將剛剛提煉的函數(shù)放置到一個更合適的類中,可以是剛剛被調(diào)用的類中,也可以創(chuàng)建新的類。

通過上面簡單兩步,我們可以將后續(xù)變化影響的范圍變小,OrderService 內(nèi)的變化將不再容易牽連到 CartService。

05 數(shù)據(jù)泥球

數(shù)據(jù)泥球指的是:多個類/方法參數(shù)中都有相同的屬性,且這些相同的屬性的業(yè)務(wù)意義也是相同的。

為什么數(shù)據(jù)泥球是代碼壞味道?

很顯然這是一種重復(fù)的表現(xiàn)。數(shù)據(jù)泥球容易造成如下問題:

  1. 涉及到屬性的調(diào)整,容易造成遺漏,需要多次調(diào)整。
  2. 降低閱讀代碼的效率,因為每次都需要從類中識別出有幾個屬性是相關(guān)的在表達(dá)一個意思。
  3. 隨著代碼的增加容易導(dǎo)致多大的類、長函數(shù)等多種壞味道。

如何解決數(shù)據(jù)泥球這種代碼壞味道?

  1. 如果類中的字段出現(xiàn)了數(shù)據(jù)泥球,對于這些重復(fù)的字段可以使用 Extract Class( 提煉類) 將關(guān)聯(lián)幾個屬性提煉到一個類中,賦予它一個業(yè)務(wù)的概念。
  2. 如果是多個方法參數(shù)中出現(xiàn)了多個重復(fù)的多個參數(shù),可以通過 Introduce Parameter Object(引入?yún)?shù)對象)將多個參數(shù)使用對象來代替,從而有效的減少重復(fù)和參數(shù)個數(shù)。
  3. 其中 2 的另外一種情況,如何調(diào)用者先通過一些邏輯生成幾個變量,再將這幾個變量通過參數(shù)傳遞給調(diào)用的方法,那么可以使用 Presere Whole Object(保持對象完整),將變量生成提煉到一個函數(shù)中,并并取消參數(shù)的傳遞,而是在被調(diào)用的方法中直接調(diào)用原本要傳遞的參數(shù)。

06 基本類型偏執(zhí)

描述的是這樣一種代碼實現(xiàn)方式:經(jīng)常使用基本數(shù)據(jù)類型,而不愿意使用對象將這些基本數(shù)據(jù)類型和其行為進(jìn)行封裝。

為什么基本類型偏執(zhí)是代碼壞味道?

首先基本類型有其作用。問題出現(xiàn)在不做場景區(qū)分場景,所有場景都是用基本數(shù)據(jù)類型去搭建業(yè)務(wù)邏輯。

問題往往出現(xiàn)在這種場景:

幾個基本數(shù)據(jù)類型共同表達(dá)意思概念,但是實現(xiàn)方式卻是像搭積木一樣,將邏輯一步步的拼接搭建起來,最終得到期望的結(jié)果。

這種實現(xiàn)的方式的問題就在于日后閱讀代碼的時候每次閱讀都需要從頭到位梳理一遍,才能清楚的其表達(dá)的意思,時間消耗有的是幾秒鐘,有的是幾分鐘,但是堆積讀幾次將會累積消耗更多的閱讀時間。問題就出現(xiàn)在不夠直白的揭示意圖。

使用幾個基本數(shù)據(jù)類型表示不同的類型,即所謂的 Type Code。

這種代碼也是存在可讀性的問題,而且非常容易導(dǎo)致 switch 語句的壞味道。

因此,并不是不能使用基本數(shù)據(jù)類型,而是應(yīng)該在揭示某個業(yè)務(wù)意圖的時候適當(dāng)?shù)氖褂梅庋b,將多個基本數(shù)據(jù)類型封裝到一個類中。從而通過對象直白的表達(dá)意圖。

如何解決基本類型偏執(zhí)這種代碼壞味道?

  1. 通過 Extract Method (提煉函數(shù))將幾個基本數(shù)據(jù)類型拼接的邏輯提煉為一個方法,比通過方法名來解釋意圖。
  2. 如果按照1做了,發(fā)現(xiàn)類中出現(xiàn)不應(yīng)該出現(xiàn)的職責(zé),那么就可以將幾個相關(guān)的基本數(shù)據(jù)類型通過 Extract Class(提煉類)將幾個基本數(shù)據(jù)類型提煉為一個類來表達(dá)一個概念,然后通過 Move Method 來講相關(guān)的操作挪動到該類中。
  3. 如果使用基本數(shù)據(jù)類型來表示狀態(tài),可以選擇使用 Replace Type Code with Class(以類取代類型碼),并將相關(guān)的操作移動到類中,避免 Switch 語句。場景可以參考《Java有限狀態(tài)機(jī)的4種實現(xiàn)對比》

07 冗贅類(Lacy Class)

這是單一職責(zé)的一個極端表現(xiàn),即拆分了很多類,每個類的職責(zé)過度單一。

為什么冗贅類是一種代碼壞味道?

因為每個類都是有閱讀成本低的,職責(zé)拆分的過細(xì),意味著多個關(guān)聯(lián)性強(qiáng)的職責(zé)也被拆分了,因此閱讀代碼來成本不一定提升,反而因為過分的分散而導(dǎo)致理解起來需要會非常費(fèi)勁。

如何解決冗贅類這種代碼壞味道?

這個壞味道也給開發(fā)者一個提醒,極端的追求某些原則同樣會導(dǎo)致不必要的麻煩,因此需要通過不斷的練習(xí)和思考來獲取平衡的這種點(diǎn)。

代碼中一旦遇到職責(zé)過度拆分的情況就可以通過 Inline Class 或者 Collapse Hierarchy 來刪除一些類,將概念合并到一個類中。

當(dāng)代碼更多的是處理業(yè)務(wù)邏輯的時候,那么其中的類應(yīng)該像領(lǐng)域語言靠近,盡量避免憑空制造一些概念,拆分職責(zé)的時候和業(yè)務(wù)相結(jié)合更有利于我們將代碼寫的簡單易讀。

08 過度耦合的消息鏈

這種代碼味道值得是不斷從獲取到的對象的子對象,導(dǎo)致很長的調(diào)用鏈。

例如

public class User {
        ...
        private Address address;
        ...
}

public class Address {
        ...
        private City city;
        ...
}

public class City {
        ...
        private PostCode postCode;
        ...
}

public class PostCode {
        ...
        private String code;
        ...
}

多度耦合的消息鏈代碼如下

String postCode = user.getAddress()
                                            .getCity()
                                            .getPostCode()
                                            .getCode();

為什么過度耦合的消息鏈?zhǔn)且环N代碼壞味道?

  1. 上面的實現(xiàn)雖然能夠正常運(yùn)行,但是會導(dǎo)致類之間的耦合,即 User 類的調(diào)用者需要在自己的內(nèi)部來獲得沒有直接練習(xí)的 postCode 的實現(xiàn);
  2. 降低了可讀性。將整個消息鏈讀完之后才能知道得到了什么,而這個過程的很多很多消息鏈中的信息是我們并不需要知道的。

如何解決過度耦合的消息鏈這種代碼壞味道?

可以通過 Extract Method 來提煉函數(shù),然后 通過 Move Method 來將提煉的方法移動到合適的位置。

如果讀過《重構(gòu)》還會提到 Hide Delegate(隱藏代理關(guān)系)的重構(gòu)手法。不過不推薦使用,因為它引入多個 Middle Man 這種實現(xiàn),當(dāng)消息鏈過長的時候,這是一個有工作量且重復(fù)的工作,另外增加了很多很多耦合性的方法。

因此可以有限照顧可讀性,通過 Extract Method 和 Move Method 來進(jìn)行重構(gòu),從而獲取實現(xiàn)和維護(hù)性上的平衡。

09 異曲同工的類

即兩個類做的同一件事或者同一類事。這種代碼很常見,比如兩個開發(fā)者同時執(zhí)行自己的開發(fā)工作,創(chuàng)建了功能類似但是方法不同的類,Code Review 的時候很容易發(fā)現(xiàn)這種代碼。

為什么異曲同工的類是一種代碼壞味道?

按照上面的描述,如果保留兩個職責(zé)類似的類會有什么不好?

  1. 后續(xù)調(diào)用實現(xiàn)類時會導(dǎo)致選擇上的疑慮,兩個類應(yīng)該選擇用哪個,而疑慮之下就是時間的浪費(fèi)。
  2. 添加代碼的時候,只向其中一個類中添加了邏輯,后續(xù)調(diào)用時 就會困擾調(diào)用者,而且容易導(dǎo)致兩個類中容易出現(xiàn)重復(fù)的代碼。

異曲同工的類是后續(xù)很多壞味道的開始。

如何解決異曲同工的類這種代碼壞味道?

  1. 一般情況,如果兩個類是一般的工具類,可以選擇使用Renove Method 和 Move Method 將類的職責(zé)描述清楚,并將相關(guān)的代碼移動到一個類中,完成兩個類的合并。

  2. 如果兩個類存并非普通的工具類而是存在一定的繼承關(guān)系,可以采用 Extract SuperClass (提煉超類)。

當(dāng)遇到代碼中的壞味道的時候,請避免延遲決策和延遲解決,因為它很可能后續(xù)導(dǎo)致其他的壞味道。及時個人意識到可以延遲決策但是放在團(tuán)隊中會可能在這個地方重復(fù)遇到問題,導(dǎo)致后續(xù)壞味道不斷被擴(kuò)散。一次一旦遇到類似的壞味道可以遵守“童子軍軍規(guī)”:讓營地比你來的時候更干凈!

10 純數(shù)據(jù)類

純數(shù)據(jù)類指的是:一個類中只有屬性和這些屬性所涉及到的 getter、setter。

為什么純數(shù)據(jù)類是一種代碼壞味道?

純數(shù)據(jù)類有其使用場景,比如 DTO 經(jīng)常這種貧血模型。但是如果結(jié)合業(yè)務(wù)到的純數(shù)據(jù)類頻繁出現(xiàn),那可不是什么好的事情,因為操作這個類中屬性的方法將會散落在各個類中,即存在者多處強(qiáng)耦合。

如何解決純數(shù)據(jù)類這種代碼壞味道?

建議使用充血模型,一個類中除了擁有屬性也應(yīng)該包含具有一定業(yè)務(wù)邏輯的行為。那么可以選擇

  1. Extract Method 將部分調(diào)用邏輯進(jìn)行提煉,提煉成一定的方法;
  2. 再使用 Move Method 將方法移動到類中,
  3. 最后 Hide Method 刪除純出局類中的 getter 和 setter。

純數(shù)據(jù)類有其使用場景,但是應(yīng)該時刻注意到哪些場景下數(shù)據(jù)類會引入壞味道,一旦發(fā)現(xiàn)盡早解決。

參考

《重構(gòu)》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。