代碼重構(六):代碼重構完整案例

来源:http://www.cnblogs.com/ludashi/archive/2016/03/25/5279459.html
-Advertisement-
Play Games

無論做什麼事情呢,都要善始善終呢。前邊連續發表了5篇關於重構的博客,其中分門別類的介紹了一些重構手法。今天的這篇博客就使用一個完整的示例來總結一下之前的重構規則,也算給之前的關於重構的博客畫一個句號。今天的示例借鑒於《重構,改善既有代碼的設計》這本書中的第一章的示例,在其基礎上做了一些修改。今天博客 ...


無論做什麼事情呢,都要善始善終呢。前邊連續發表了5篇關於重構的博客,其中分門別類的介紹了一些重構手法。今天的這篇博客就使用一個完整的示例來總結一下之前的重構規則,也算給之前的關於重構的博客畫一個句號。今天的示例借鑒於《重構,改善既有代碼的設計》這本書中的第一章的示例,在其基礎上做了一些修改。今天博客從頭到尾就是一個完整的重構過程。首先會給出需要重構的代碼,然後對其進行分析,然後對症下藥,使用之前我們分享的重構規則對其進行一步步的重構。

先來聊一下該示例的使用場景(如果你有重構這本書的話,可以參加第一章中的示例,不過本博客中的示例與其有些出入)。就是一個客戶去DVD出租的商店裡進行消費,下方的程式是給店主用的,來根據用戶所借的不同的DVD種類和數量來計算該用戶消費的金額和積分。需求很簡單而且也不難理解。今天博客會給出原始的代碼,也是需要進行重構的代碼。當然原始代碼完全符合需求,並且可以正確執行。廢話少說,先看示例吧。

 

一、需要重構的代碼

在本篇博客的第一部分,我們先給出完成上述需求需要重構的代碼。然後在此基礎上進行分析,使用之前我們提到過的重構手法進行重構。首先我們給出了電影類的實現。在Movie類中有電影的種類(靜態常量):普通電影、兒童電影、新電影,然後有兩個成員變數/常量是priceCode(價格代碼)、title(電影名稱),最後就是我們的構造方法了。該Movie類比較簡單,在此就不做過多的贅述了。

 

實現完Movie類接下來就是租賃類Rental,這個Rental類的職責就是負責統計某個電影租賃的時間。下方就是這個租賃類,該類也是比較簡單的,其中有兩個欄位,一個是租了的電影,另一個就是租賃的時間了。

  

接下來要實現我們的消費者類了,也就是Customer類。在Customer類中有消費者的名字name和一個數組,該數組中寸的就是租賃電影的集合。其中的statement()方法就是結算該客戶的結算信息的方法,並將結果進行列印。在此我們需要瞭解的需求是每種電影的計價方式以及積分的計算規則。

電影價格計算規則:

  普通片兒--2天之內含2天,每部收費2元,超過2天的部分每天收費1.5

  新片兒--每天每部3 

  兒童片--3天之內含3天,每部收費1.5元,超過3天的部分每天收費1.5

積分計算規則:

       每借一步電影積分加1,新片每部加2

statement()函數中所做的事情就是根據上面的計算規則,根據用戶所租賃的電影的不同來進行金額的計算和積分的計算的。

  

如果你看代碼不太直觀的話,下麵我使用了startUML簡單的畫了一個UML的類圖來說明上述三個類中的依賴關係。具體如下所示:

 

在對上面代碼重構之前呢,我們還必須有上述代碼的測試用例。因為在每次重構之前,我們修改的是代碼的內部結構,而代碼模塊對外的調用方式不會變的。所以我們所創建的測試用例可以幫助驗證我們重構後的程式是否可以正常的工作,是否重構後還符合我們的需求。下方就是我們創建的測試用例(當然,在iOS開發中你可以使用其他的測試框架來進行單元測試,重構時,單元測試是少不了的)。在本篇博客中重構後的代碼仍然使用下方的測試用例。

 1 //測試用例--------------------------------------------------------------------
 2 //創建用戶
 3 let customer = Customer(name: "ZeluLi")
 4 
 5 //創建電影
 6 let regularMovie:Movie = Movie(title: "《老炮兒》", priceCode: Movie.REGULAR)
 7 let newMovie:Movie = Movie(title: "《福爾摩斯》", priceCode: Movie.NEW_RELEASE)
 8 let childrenMovie:Movie = Movie(title: "《葫蘆娃》", priceCode: Movie.CHILDRENS)
 9 
10 //創建租賃數據
11 let rental1:Rental = Rental(movie: regularMovie, daysRented: 5)
12 let rental2:Rental = Rental(movie: newMovie, daysRented: 8)
13 let rental3:Rental = Rental(movie: childrenMovie, daysRented: 2)
14 
15 customer.rentals.append(rental1)
16 customer.rentals.append(rental2)
17 customer.rentals.append(rental3)
18 
19 let result = customer.statement()
20 print(result)

針對上述案例,上面測試用例的輸出結果如下。在每次重構後,我們都會執行上述測試代碼,然後觀察結果是否與之前的相同。當然如果你的是單元測試的話,完全可以把對結果檢查的工作交給單元測試中的斷言來做。

    

 

二、重構1:對較statement函數進行拆分

1.對statement()函數使用“Extract Method”原則

在上面的案例中,最不能容忍的,也就是最需要重構的首先就是Customer中的statement()函數。statement()函數最大缺點就是函數裡邊做的東西太多,我們第一步需要做的就是對其進行拆分。也就是使用我們之前提到過的“Extract Method”(提煉函數)原則對該函數進行簡化和拆分。將statement()中可以獨立出來的模塊進行提取。經過分析後的,我們不難發現下方紅框當中的代碼是一個完整的模塊,一個是進行單價計算的,一個是進行積分計算的,我們可以將這兩塊代碼進行提取並封裝成一個新的方法。在封裝新方法時,要給這個新的方法名一個恰當的函數名,見名知意。

   

下方這塊代碼就是我們對上面這兩個紅框中的代碼的提取。在提取時,將依賴於statement()函數中的數據作為新函數的參數即可。封裝後的方法如下,在statement函數中相應的地方調用下方的方法即可。下方就是我們封裝的計算當前電影金額和計算積分的函數。這兩個函數都需要傳入一個Rental的對象。

    //根據租賃訂單,計算當前電影的金額
    func amountFor(aRental: Rental) -> Double {
        
        var result:Double = 0       //單價變數
        
        switch aRental.movie.priceCode {
            case Movie.REGULAR:
                result += 2
                if aRental.daysRented > 2 {
                    result += Double(aRental.daysRented - 2) * 1.5
                }
            case Movie.NEW_RELEASE:
                result += Double(aRental.daysRented * 3)
            case Movie.CHILDRENS:
                result += 1.5
                if aRental.daysRented > 3 {
                    result += Double(aRental.daysRented - 3) * 1.5
                }
            default:
                break
        }
        return result
    }
    

     //計算當前電影的積分
    func getFrequentRenterPoints(rental: Rental) -> Int {
        var frequentRenterPoints: Int = 0               //用戶積分
        frequentRenterPoints++
        if rental.movie.priceCode == Movie.NEW_RELEASE &&
            rental.daysRented > 1{
                frequentRenterPoints++
        }
        return frequentRenterPoints
    }

經過上面的重構步驟,我們會運行一下測試用例或者執行一下單元測試,看是否我們的重構過程引起了新的bug。

 

三、重構2:將相應的方法移到相應的類中

經過上面的重構,我們從statement()函數中提取了兩個方法。觀察這兩個重構後的方法我們不難看出,這兩個封裝出來的新的方法都需要一個參數,這個參數就是Rental類的對象。也就是這兩個方法都依賴於Rental類,而對該函數所在的當前類不太感冒。出現這種情況的原因就是這兩個函數放錯了地方,因為這兩個函數放在Customer類中不依賴與Customer類而依賴於Rental類,那就足以說明這兩個方法應該放在Rental類中。

經過我們簡單的分析後,我們就可以決定要將我們新提取的方法放到Rental類中,並且函數的參數去掉。因為函數在Rental類中,所以在函數中直接使用self即可。將計算金額的方法和計算積分的方法移到Rental類中後,我們的Rental類如下所示。在我們的Customer中的statement()方法中在計算金額和計算積分時,直接調用Rental中的方法即可。經過這一步重構後,不要忘記執行一下你的測試用例,監測一下重構的結果是否正確。

   

 

四、使用“以查詢取代臨時變數”再次對statement()函數進行重構

經過第二步和第三步的重構後,Customer中的statement()函數如下所示。在計算每部電影的金額和積分時,我們調用的是Rental類的對象的相應的方法。下方的方法與我們第一部分的方法相比可謂是簡潔了許多,而且易於理解與維護。

   

不過上面的代碼仍然有重構的空間,舉個例子,如果我們要將結果以HTML的形式進行組織的話,我們需要將上面的代碼進行複製,然後修改result變數的文本組織方式即可。但是這樣的話,其中的好多臨時變數也需要被覆制一份,這是完全相同的,這樣就容易產生重覆的代碼。在這種情況下,我們需要使用“Replace Temp with Query”(已查詢取代臨時變數)的重構手法來取出上面紅框中的臨時變數。

上面紅框中的每個臨時變數我們都會提取出一個查詢方法,下方是使用“Replace Temp with Query”(已查詢取代臨時變數)規則重構後的statement()函數,以及提取的兩個查詢函數。

   

經過上面這些步驟的重構,我們的測試用例依然不變。在每次重構後我們都需要調用上述的測試用例來檢查重構是否產生了副作用。現在我們的類間的依賴關係沒怎麼發生變化,只是相應類中的方法有些變化。下方是現在代碼所對應的類圖,因為在上述重構的過程中我們主要做的是對函數的重構,也就是對函數進行提取,然後將提取的函數放到相應的類中,從下方的簡化的類圖中就可以看出來了。

   

 

五. 繼續將相應的函數進行移動(Move Method)

對重構後的代碼進行觀察與分析,我們任然發現在Rental類中的getCharge()函數中的內容與getFrequentRenterPoints()函數中的內容對Movie類的依賴度更大。因為這兩個函數都只用到了Rental類中的daysRented屬性,而多次用到了Movie中的內容。因此我們需要將這兩個函數中的內容移到Movie類中更為合適。所以我繼續講該部分內容進行移動。

移動的方法是保留Rental中這兩個函數的聲明,在Movie中創建相應的函數,將函數的內容移到Movie中後,再Rental中調用Movie中的方法。下方是我們經過這次重構後我們Movie類中的內容。其中紅框中的內容是我們移過來的內容,而綠框中的參數需要從外界傳入。

     

將相應的方法體移動Movie類中後,在Rental中我們需要對其進行調用。在調用相應的方法時傳入相應的參數即可。下方就是經過這次中國Rental類的代碼,綠框中的代碼就是對Movie中新添加的方法的調用。

     

經過上面的重構,我們的方法似乎是找到了歸宿了。重構就是這樣,一步步來,不要著急,沒動一步總是要向著好的方向發展。如果你從第一部分中的代碼重構到第五部分,似乎有些困難。經過上面這些間接的過程,感覺也是挺愉快的蠻。下方是經過我們這次重構的類圖。

   

 

六、使用“多態”取代條件表達式

在我們之前的博客中對條件表達式進行重構時,提到了使用類的多態對條件表達式進行重構。接下來我們就要使用該規則對Movie類中的getCharge()與getFrequentRenterPoints()函數進行重構。也就是使用我們設計模式中經常使用的“狀態模式”。在該部分我們不需要對Rental類和Customer類進行修改,只對Movie類修改,並且引入相應的介面和繼承關係。

我們對Movie類中的getCharge()方法中的Switch-Case結構觀察時,我們很容易發現,此處完全可以使用類的多態來替代(具體請參見《代碼重構(四):條件表達式重構規則(Swift版)》)。具體實現方式是將不通的價格計算方式提取到我們新創建的價格類中,每種電影都有自己價格類,而這些價格類都實現同一個介面,這樣一來在Movie中就可以使用多態來獲取價格了。積分的計算也是一樣的。下方是我們要實現結構的類圖。下方紅框中是在原來基礎上添加的新的介面和類,將條件表達式所處理的業務邏輯放在了我們新添加的類中。這樣我們就可以使用類的多態了,而且遵循了“單一職責”。

   

 

下方代碼就是上面大的紅框中所對應的代碼實現。Price是我們定義好的協議,在協議中規定了遵循該協議的類要實現的方法。而在每個具體實現類中實現了相同的介面,但是不同的類中相同的方法做的事情不同。在不同的類中的getCharge()中要做的事情就是Switch-Case語句中所處理的數據。

   

添加上上面的結構以後,在麽我們的Movie中就可以使用多態了,在Movie中添加了一個Price聲明的對象,我們會根據不同的priceCode來給price變數分配不同的對象。而在getCharge()中只管調用price的getCharge()函數即可,具體做法如下。

   

 

今天的博客到這兒也就差不多了,其實上面的代碼仍然有重構的空間,如果我們想把Switch-Case這個結構去掉的話,我們可以在上面代碼的基礎上創建多個工廠方法即可。在此就不過贅述了。

如果看完今天的博客的內容不夠直觀的話,那麼請放心。本篇博客中每次重構過程的完整實例會在github上進行分享。對每次重構的代碼都進行了系統的整理。今天博客中的代碼整理的結果如下。

  

 

github分享地址為:https://github.com/lizelu/CodeRefactoring-Swift

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、親密性原則 將相關的內容組織到一起,通過字體大小,留白分層等區分內容關係 改變成 如果通過間距就可以劃分區域,我們就不用線條,框架去劃分區域,這樣更加簡潔 改變成 註:1、透明度的改變:增強背景融合性與親密性 2、方向性:有指向的線條(色塊的利用可以產生視覺衝擊) 3、顏色:不要使用純黑色(可以 ...
  • 建造者模式(Builder) 類圖 描述 建造者: Builder:定義一個建造者抽象類,以規範產品對象的各個組成部分的建造。這個介面規定要實現對象的哪些部分的創建,並不涉及具體的對象部件的創建。 ConcreteBuilder:繼承Builder,針對不同的業務邏輯,具體化對象的各部分的創建。在建 ...
  • 對基於請求的分散式消息樹的分析 在MVC時有過濾器System.Web.Mvc.ActionFilterAttribute,它可以對action執行的整個過程進行攔截,執行前與執行後我們可以註入自己的代碼,這是我們實現對請求做監控的前提,對於一個請求來說,如果它是從Get或者Post過來的,我們會在 ...
  • 數據在分片時,典型的是分庫分表,就有一個全局ID生成的問題。單純的生成全局ID並不是什麼難題,但是生成的ID通常要滿足分片的一些要求: 1 不能有單點故障。 2 以時間為序,或者ID里包含時間。這樣一是可以少一個索引,二是冷熱數據容易分離。 3 可以控制ShardingId。比如某一個用戶的文章要放 ...
  • 設計模式 目錄 UML類圖 簡單工廠模式(Simple Factory) 創建型: 工廠方法模式(Factory Method) 抽象工廠模式(Abstract Factory) 建造者模式(Builder) 原型模式(Prototype) 單例模式(Singleton) 結構型: 適配器模式(Ad ...
  • 抽象工廠模式(Abstract Factory) 類圖 描述 抽象工廠: 多個抽象產品類,每個抽象產品類可以派生出多個具體產品類; 一個抽象工廠類,可以派生出多個具體工廠類; 每個具體工廠可以創建多個具體產品,即每個工廠可以生產一個產品集合。 應用場景 就拿生產轎車來說,轎車是由發動機、車輪、車體結 ...
  • 工廠方法模式(Factory Method) 類圖 描述 工廠方法: 一個抽象產品類,可以派生多個具體產品類; 一個抽象工廠類,可以派生多個具體工廠類; 每個具體工廠只能創建一個具體產品。 應用場景 汽車介面 汽車類 汽車工廠介面 汽車工廠類 調用 ...
  • 簡單工廠模式(Simple Factory) 類圖 描述 簡單工廠: 一個抽象產品類,可以派生多個具體產品類; 一個具體工廠類; 工廠只能創建一個具體產品。 應用場景 汽車介面 汽車類 汽車工廠類 調用,從配置文件中讀取操作符 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...