重構(gòu)-改善既有代碼的結(jié)構(gòu)

重構(gòu)

一.何謂重構(gòu)特幔?

重構(gòu):使用一系列重構(gòu)手法蓖柔,在不改變軟件可觀察行為的前提下辰企,調(diào)整其結(jié)構(gòu),提高其可理解性况鸣,降低其修改成本牢贸。

二.為什么要重構(gòu)?

  • 重構(gòu)改進(jìn)軟件設(shè)計(jì)
  • 重構(gòu)使軟件更容易理解
  • 重構(gòu)幫助找到BUG
  • 重構(gòu)提高編程速度

三.何時(shí)重構(gòu)镐捧?

  • 三次法則:事不過三潜索,三則重構(gòu)
  • 添加功能時(shí)重構(gòu)
  • 修復(fù)BUG時(shí)重構(gòu)
  • Review代碼時(shí)重構(gòu)

四.何時(shí)不該重構(gòu)?

  • 無法穩(wěn)定運(yùn)行直接重寫不用重構(gòu)
  • 項(xiàng)目已經(jīng)接近最后期限懂酱,不應(yīng)該重構(gòu)竹习,雖然重構(gòu)能夠提高生產(chǎn)力,但是你沒有足夠的時(shí)間列牺,這通常標(biāo)示你其實(shí)早該進(jìn)行重構(gòu)了整陌。

五.代碼的壞味道

  1. Duplicated Code 重復(fù)代碼
  2. Long Method 過長(zhǎng)函數(shù)
  3. Large Class 過大的類
  4. Long Parameter List 過長(zhǎng)參數(shù)列表
  5. Divergent Change 發(fā)散式變化
  6. Shotgun Surgery 散彈式修改
  7. Feature Envy 依戀情結(jié)
  8. Data Clumps 數(shù)據(jù)泥團(tuán)
  9. Primitive Obsession 基本類型偏執(zhí)
  10. Switch Statements switch驚悚現(xiàn)身 (使用多態(tài)性替換)
  11. Parallel Inheritance Hierarchies 平行繼承體系
  12. Lazy Class 冗贅類
  13. Speculative Generality 夸夸其談未來性
  14. Temporary Field 令人迷惑的暫時(shí)字段
  15. Message Chains 過渡耦合的消息鏈
  16. Middle Man 中間人
  17. Inappropriate Intimacy 狎昵關(guān)系
  18. Alternative Classes with Different Interfaces 異曲同工的類
  19. Incomplete Library Class 不完美的庫(kù)類
  20. Data Class 純稚的數(shù)據(jù)類
  21. Refused Bequest 被拒絕的遺贈(zèng)
  22. Comments 過多的注釋

五.常用的重構(gòu)手法

5.1 Extract Method(提煉函數(shù))

    func printOwing(amount: Double) {
        printBanner()
        
        //print details
        print("name:", self.name)
        print("amount:", amount)
    }
    func printOwing(amount: Double) {
        printBanner()
        
        printDetails(amount: amount)
    }
    
    func printDetails(amount: Double) {
        //print details
        print("name:", self.name)
        print("amount:", amount)
    }

動(dòng)機(jī)

當(dāng)一個(gè)函數(shù)過長(zhǎng)或者需要注釋才能看懂的代碼,可以將這段代碼放入一個(gè)獨(dú)立的函數(shù)中,并給他一個(gè)合適的命名泌辫。
簡(jiǎn)短而命名良好的函數(shù)的優(yōu)點(diǎn):函數(shù)粒度越小随夸,它被復(fù)用的幾率越大;其次甥郑,這會(huì)使高層函數(shù)讀起來像一系列注釋逃魄;再次,如果函數(shù)都是細(xì)粒度澜搅,子類重載父類函數(shù)也會(huì)容易得多伍俘。

做法

  1. 創(chuàng)建一個(gè)新函數(shù),根據(jù)這個(gè)函數(shù)的意圖來給他命名(以“做什么”來命名勉躺,而不要以“怎么做命名”)癌瘾。
    2.將要提煉的代碼從源函數(shù)復(fù)制到新建的目標(biāo)函數(shù)中。
    3.將被提煉代碼中需要讀取的局部變量饵溅,作為參數(shù)傳給目標(biāo)函數(shù)妨退。
    4.處理完所有局部變量后,進(jìn)行編譯蜕企。
    5.在源函數(shù)中咬荷,將被提煉的代碼段替換為對(duì)目標(biāo)函數(shù)的調(diào)用。
    6.編譯轻掩,測(cè)試幸乒。

5.2 Inline Method(內(nèi)聯(lián)函數(shù))

    func getRating() -> Int {
        return valueMoreThanFive() ? 2 : 1
    }
    
    func valueMoreThanFive() -> Bool {
        return self.value > 5
    }
    func getRating() -> Int {
        return (self.value > 5) ? 2 : 1
    }

動(dòng)機(jī)

某些函數(shù),其內(nèi)部代碼和它的名稱一樣清晰易讀唇牧。
你手上有一群組織不合理的函數(shù)罕扎,你可以先把它們內(nèi)聯(lián)到一個(gè)大的函數(shù)里,在從中提煉出組織合理的小型函數(shù)丐重。
使用了太多的中間層腔召,使得系統(tǒng)中所有的函數(shù)只是對(duì)另外一個(gè)函數(shù)的簡(jiǎn)單調(diào)用,造成在這些調(diào)用中暈頭轉(zhuǎn)向扮惦。

做法

  1. 檢查函數(shù)臀蛛,確定它不具有多態(tài)性。

如果有子類重載它径缅,不要將它內(nèi)聯(lián)掺栅,因?yàn)椴荒苤剌d一個(gè)不存在的函數(shù)

2.找到這個(gè)函數(shù)所有被調(diào)用的點(diǎn)。
3.將這個(gè)函數(shù)所有被調(diào)用點(diǎn)都替換為函數(shù)本體纳猪。
4.編譯,測(cè)試桃笙。
5.刪除該函數(shù)的定義氏堤。

5.2 Move Method(搬移函數(shù))

搬移函數(shù)

動(dòng)機(jī)

“搬移函數(shù)”是重構(gòu)理論的支柱。如果有一個(gè)類有太多行為,或如果一個(gè)類與另一個(gè)類有太多合作而形成高度耦合鼠锈,就應(yīng)該使用搬移函數(shù)闪檬。
一個(gè)函數(shù)使用另一個(gè)對(duì)象的次數(shù)比使用自己所駐的對(duì)象還要多,考慮是否把這個(gè)函數(shù)搬移到另一個(gè)對(duì)象所屬的類中购笆。

做法

  1. 檢查源類中被源函數(shù)使用的一切特性(包括字段和函數(shù))粗悯,考慮他們是否也該被搬移。

如果某個(gè)特性只被你打算搬移的函數(shù)用到同欠,就應(yīng)該將它一并搬移样傍。如果有另外的函數(shù)使用了這個(gè)特性,你可以考慮將該特性的所有函數(shù)全都一并搬移铺遂。有時(shí)候衫哥,搬移一組函數(shù)比逐一搬移簡(jiǎn)單些。

  1. 檢查源類的子類和父類襟锐,看看是否有該函數(shù)的其它聲明撤逢。
  2. 在目標(biāo)類中聲明這個(gè)函數(shù)。

你可以為此函數(shù)選擇一個(gè)新名稱——對(duì)目標(biāo)類更有意義的名稱

  1. 將源函數(shù)的代碼復(fù)制到目標(biāo)函數(shù)中粮坞。調(diào)整目標(biāo)函數(shù)蚊荣,使其能在新家正常運(yùn)行。
  2. 編譯目標(biāo)類莫杈。
  3. 決定如何從源函數(shù)正確引用目標(biāo)對(duì)象互例。
  4. 修改源函數(shù),使之成為一個(gè)純委托函數(shù)姓迅。
  5. 編譯敲霍,測(cè)試。
  6. 決定是否刪除源函數(shù)丁存,或者將它作為一個(gè)委托函數(shù)保留下來肩杈。
  7. 如果要移除源函數(shù),將源類中對(duì)源函數(shù)的所有調(diào)用解寝,替換為對(duì)目標(biāo)函數(shù)的調(diào)用扩然。
  8. 編譯,測(cè)試聋伦。

5.2 Rename Method(函數(shù)改名)

Rename Method

動(dòng)機(jī)

函數(shù)的名稱應(yīng)該準(zhǔn)確地表達(dá)它的用途夫偶。給函數(shù)命名有一個(gè)好方法:首先考慮應(yīng)該給這個(gè)函數(shù)寫上一句怎樣的注釋,然后想辦法將注釋變成函數(shù)名稱觉增。
如果你看到一個(gè)函數(shù)名稱不能很好地表達(dá)它的用途兵拢,應(yīng)該馬上加以修改。好的函數(shù)名可以讓其它人更容易讀懂你的代碼逾礁。要想成為一個(gè)編程高手说铃,起名的水平是至關(guān)重要的。

做法

  1. 檢查函數(shù)是否被父類或子類實(shí)現(xiàn)過。如果是腻扇,需要針對(duì)每個(gè)實(shí)現(xiàn)分別進(jìn)行下列步驟债热。
  2. 聲明一個(gè)新函數(shù),將它命名為你想要的新名稱幼苛。將舊函數(shù)的代碼復(fù)制到新函數(shù)中窒篱,并進(jìn)行適當(dāng)調(diào)整。
  3. 編譯舶沿。
  4. 修改舊函數(shù)墙杯,讓它掉用新函數(shù)。
  5. 編譯暑椰,測(cè)試霍转。
  6. 找出舊函數(shù)所有被引用點(diǎn),將它們?nèi)啃薷臑閷?duì)新函數(shù)的引用一汽。每次修改后避消,編譯并測(cè)試。
  7. 刪除舊函數(shù)召夹。

如果舊函數(shù)是該類public接口的一部分岩喷,你可能無法安全地刪除它。這種情況下监憎,將它保留在原地纱意,并將它標(biāo)示為deprecated。

  1. 編譯鲸阔,測(cè)試偷霉。

5.2 Add Parameter(添加參數(shù))

為此函數(shù)添加一個(gè)對(duì)象參數(shù),讓該對(duì)象帶進(jìn)函數(shù)所需信息褐筛。

Add Parameter

動(dòng)機(jī)

你必須修改一個(gè)函數(shù)类少,而修改后的函數(shù)需要一些過去沒有的信息,因此你需要給該函數(shù)添加一個(gè)參數(shù)渔扎。

做法

  1. 檢查函數(shù)是否被父類或子類實(shí)現(xiàn)過硫狞。如果是,需要針對(duì)每個(gè)實(shí)現(xiàn)分別進(jìn)行下列步驟晃痴。
  2. 聲明一個(gè)新函數(shù)残吩,名稱與原函數(shù)相同,只是加上新添參數(shù)倘核。將舊函數(shù)的代碼復(fù)制到新函數(shù)中泣侮。
  3. 編譯。
  4. 修改舊函數(shù)紧唱,讓它掉用新函數(shù)旁瘫。
  5. 編譯祖凫,測(cè)試琼蚯。
  6. 找出舊函數(shù)所有被引用點(diǎn)酬凳,將它們?nèi)啃薷臑閷?duì)新函數(shù)的引用。每次修改后遭庶,編譯并測(cè)試宁仔。
  7. 刪除舊函數(shù)。

如果舊函數(shù)是該類public接口的一部分峦睡,你可能無法安全地刪除它翎苫。這種情況下,將它保留在原地榨了,并將它標(biāo)示為deprecated煎谍。

  1. 編譯,測(cè)試龙屉。

5.2 Remove Parameter(移除參數(shù))

函數(shù)本體不再需要某個(gè)參數(shù)呐粘,將該參數(shù)去除

Remove Parameter

動(dòng)機(jī)

程序員可能經(jīng)常添加參數(shù)晚吞,確往往不愿意去掉它們计济。因?yàn)槎嘤嗟膮?shù)不會(huì)引起任何問題,而且以后還可能用上它胁住。
但是對(duì)于多態(tài)函數(shù)五芝,情況有所不同痘儡。可能多態(tài)函數(shù)的另一個(gè)實(shí)現(xiàn)會(huì)使用這個(gè)參數(shù)枢步,此時(shí)你就不能去除它沉删。

做法

  1. 檢查函數(shù)是否被父類或子類實(shí)現(xiàn)過。如果是醉途,需要針對(duì)每個(gè)實(shí)現(xiàn)分別進(jìn)行下列步驟矾瑰。
  2. 聲明一個(gè)新函數(shù),名稱與原函數(shù)相同结蟋,只是去除不必要的參數(shù)脯倚。將舊函數(shù)的代碼復(fù)制到新函數(shù)中。
  3. 編譯嵌屎。
  4. 修改舊函數(shù)推正,讓它調(diào)用新函數(shù)。
  5. 編譯宝惰,測(cè)試植榕。
  6. 找出舊函數(shù)所有被引用點(diǎn),將它們?nèi)啃薷臑閷?duì)新函數(shù)的引用尼夺。每次修改后尊残,編譯并測(cè)試炒瘸。
  7. 刪除舊函數(shù)。

如果舊函數(shù)是該類public接口的一部分寝衫,你可能無法安全地刪除它顷扩。這種情況下,將它保留在原地慰毅,并將它標(biāo)示為deprecated隘截。

  1. 編譯,測(cè)試汹胃。

5.2 Parameterize Method(令函數(shù)攜帶參數(shù))

若干函數(shù)做了類似的工作婶芭,但在函數(shù)名稱中卻包含了不同的值。建立單一函數(shù)着饥,以參數(shù)表達(dá)那些不同的值

Parameterize Method

動(dòng)機(jī)

你可能會(huì)發(fā)現(xiàn)兩個(gè)函數(shù)犀农,它們做著類似的工作,但因少數(shù)幾個(gè)值致使行為略有不同宰掉。

做法

  1. 新建一個(gè)帶參數(shù)的函數(shù)呵哨,使它可以替換先前所有的重復(fù)性函數(shù)。
  2. 編譯贵扰。
  3. 將調(diào)用舊函數(shù)的代碼改為調(diào)用新函數(shù)仇穗。
  4. 編譯,測(cè)試戚绕。
  5. 對(duì)所有舊函數(shù)重復(fù)上述步驟纹坐,每次替換后,修改并測(cè)試舞丛。
  6. 刪除舊函數(shù)耘子。

如果舊函數(shù)是該類public接口的一部分,你可能無法安全地刪除它球切。這種情況下谷誓,將它保留在原地,并將它標(biāo)示為deprecated吨凑。

  1. 編譯捍歪,測(cè)試。

5.2 Replace Parameter with Explicit Methods(以明確函數(shù)取代參數(shù))

你有一個(gè)函數(shù)鸵钝,其中完全根據(jù)參數(shù)值不同而采取不同的行為糙臼。針對(duì)該參數(shù)的每一個(gè)可能值,建立一個(gè)獨(dú)立函數(shù)
重構(gòu)前:

    func setValue(name: String, value: CGFloat) {
        if name == "height" {
            self.height = value
        } else if name == "width" {
            self.width = value
        }
    }

重構(gòu)后:

    func setHeight(value: CGFloat) {
        self.height = value
    }
    
    func setWidth(value: CGFloat) {
        self.width = value
    }

動(dòng)機(jī)

如果某個(gè)參數(shù)有多種可能的值恩商,而函數(shù)內(nèi)又以條件表達(dá)式檢查這些參數(shù)值变逃,并根據(jù)不同參數(shù)值做出不同的行為,那么就應(yīng)該使用本項(xiàng)重構(gòu)怠堪。
接口更清晰揽乱,相比之下名眉,Switch.setOn()比Switch.setState(true)要清楚得多。

做法

  1. 針對(duì)參數(shù)的每一種可能值凰棉,新建一個(gè)明確函數(shù)损拢。
  2. 修改條件表達(dá)式的每個(gè)分支,使其調(diào)用合適的新函數(shù)渊啰。
  3. 修改每個(gè)分支后探橱,編譯并測(cè)試。
  4. 修改原函數(shù)的每一個(gè)被調(diào)用點(diǎn)绘证,讓它們調(diào)用合適的新函數(shù)。
  5. 編譯哗讥,測(cè)試嚷那。
  6. 所有調(diào)用端都修改完畢后,刪除原函數(shù)杆煞。

5.2 Preserve Whole Objcet(保持對(duì)象完整)

你把某個(gè)對(duì)象的若干值作為函數(shù)調(diào)用時(shí)的參數(shù)魏宽,改為傳遞整個(gè)對(duì)象
重構(gòu)前:

        let low = daysTempRange.low
        let high = daysTempRange.high
        withinPlan = plan.withinRange(low, high)

重構(gòu)后:

        withinPlan = plan.withinRange(daysTempRange)

動(dòng)機(jī)

有時(shí)候,你會(huì)將同一對(duì)象的若干數(shù)據(jù)項(xiàng)作為參數(shù)决乎,傳遞給某個(gè)函數(shù)队询。這樣做的問題在于:萬(wàn)一將來被調(diào)用函數(shù)需要新的數(shù)據(jù)項(xiàng),你就必須查找并修改對(duì)此函數(shù)的所有調(diào)用构诚。
使參數(shù)列表變短蚌斩,提高代碼可讀性。

弊端:如果你傳的是數(shù)值范嘱,被調(diào)用的函數(shù)只依賴于這些數(shù)值送膳。如果你傳遞的是整個(gè)對(duì)象,被調(diào)用的函數(shù)所在的對(duì)象就需要依賴參數(shù)對(duì)象丑蛤。

5.2 Replace Parameter with Methods(以函數(shù)取代參數(shù))

對(duì)象調(diào)用某個(gè)函數(shù)叠聋,并將所得結(jié)果作為參數(shù),傳遞給另一個(gè)函數(shù)受裹。而另一個(gè)函數(shù)本身也能調(diào)用前一個(gè)函數(shù)碌补。讓參數(shù)接受者去除該項(xiàng)參數(shù),并直接調(diào)用前一個(gè)函數(shù)
重構(gòu)前:

        let basePrice = quantity * itemPrice
        let discountLevel = self.getDiscountLevel()
        let finalPrice = discountedPrice(basePrice, discountLevel)

重構(gòu)后:

        let basePrice = quantity * itemPrice
        let finalPrice = discountedPrice(basePrice)

動(dòng)機(jī)

如果函數(shù)可以通過其它途徑獲得參數(shù)值棉饶,那么它就不應(yīng)該通過參數(shù)取得該值厦章。

做法

  1. 如果有必要,使用Extract Method將參數(shù)的計(jì)算過程提煉到一個(gè)獨(dú)立函數(shù)中砰盐。
  2. 將函數(shù)本地內(nèi)引用該參數(shù)的地方改為調(diào)用新建的函數(shù)闷袒。
  3. 每次替換后,編譯并測(cè)試岩梳。
  4. 全部替換完成后囊骤,使用Remove Parameter將該參數(shù)去掉晃择。

5.2 Introduce Parameter Object(引入?yún)?shù)對(duì)象)

某些參數(shù)總是很自然地同時(shí)出現(xiàn)

Introduce Parameter Object

動(dòng)機(jī)

你常會(huì)看到特定的一組參數(shù)總是一起被傳遞,可以運(yùn)用一個(gè)對(duì)象包裝所有這些數(shù)據(jù)也物。
當(dāng)你把這些參數(shù)組織到一起后宫屠,往往很快可以發(fā)現(xiàn)一些行為可被移至新建的類中。

做法

  1. 新建一個(gè)類滑蚯,用以表現(xiàn)你想替換的一組參數(shù)浪蹂。
  2. 編譯。
  3. 針對(duì)使用該組參數(shù)的所有函數(shù)告材,使用Add Parameter坤次,傳入新建類的實(shí)例對(duì)象,并將此參數(shù)值設(shè)置為null斥赋。
  4. 對(duì)于這些參數(shù)中的每一項(xiàng)缰猴,從函數(shù)名中移除掉,并修改調(diào)用端和函數(shù)本體疤剑,讓它們都通過新的參數(shù)對(duì)象取得該值滑绒。
  5. 每去除一個(gè)參數(shù),編譯并測(cè)試隘膘。
  6. 將原先的參數(shù)全部去掉之后疑故,觀察有無適當(dāng)?shù)暮瘮?shù)可以運(yùn)用Move Method搬移到參數(shù)對(duì)象之中。

被搬移的可以是整個(gè)函數(shù)弯菊,也可以是某個(gè)函數(shù)的部分代碼纵势。如果是后者,需要先用Extract Method將該部分代碼提煉到一個(gè)新的函數(shù)中误续。

5.2 Hide Method(隱藏函數(shù))

有一個(gè)函數(shù)吨悍,從來沒有被其他任何類用到,將這個(gè)函數(shù)修改為private

image.png

動(dòng)機(jī)

一個(gè)類蹋嵌,暴露給其他類的接口越多育瓜,越容易產(chǎn)生耦合。在一個(gè)函數(shù)確實(shí)需要被其他類訪問之前栽烂,應(yīng)該讓它對(duì)其他類不可見躏仇。

做法

  1. 經(jīng)常檢查有沒有可能降低某個(gè)函數(shù)的可見度。
  2. 盡可能降低所有函數(shù)的可見度腺办。
  3. 每完成一組函數(shù)的隱藏之后焰手,編譯并測(cè)試。

5.2 Replace Constructor With Factory Method(以工廠函數(shù)取代構(gòu)造函數(shù))

你希望在創(chuàng)建對(duì)象時(shí)不僅僅是簡(jiǎn)單的構(gòu)建動(dòng)作怀喉,將構(gòu)造函數(shù)替換為工廠函數(shù)
重構(gòu)前:

    Employee(type: Int) {
        self.type = type
    }

重構(gòu)后:

    static func create(type: Int) -> Employee {
        return Employee(type: type)
    }

動(dòng)機(jī)

最明顯的動(dòng)機(jī)就是书妻,在派生子類的過程中以工廠函數(shù)取代類型碼。

做法

  1. 新建一個(gè)工廠函數(shù)躬拢,讓它調(diào)用現(xiàn)有的構(gòu)造函數(shù)躲履。
  2. 將調(diào)用構(gòu)造函數(shù)的代碼改為調(diào)用工廠函數(shù)见间。
  3. 每次替換后,編譯并測(cè)試工猜。
  4. 將構(gòu)造函數(shù)聲明為private米诉。
  5. 編譯。

5.2 Pull Up Field(字段上移)

兩個(gè)子類擁有相同的字段篷帅,將該字段移至父類

Pull Up Field

動(dòng)機(jī)

如果子類是分別開發(fā)的史侣,或者是在重構(gòu)過程中組合起來的,你常會(huì)發(fā)現(xiàn)它們擁有重復(fù)性魏身,特別是字段更容易重復(fù)惊橱。如果它們重復(fù)了,你就可以將它們歸納到父類中去叠骑。

做法

  1. 針對(duì)待提升的字段李皇,檢查它們的所有被使用點(diǎn),確認(rèn)它們以同樣的方式被使用宙枷。
  2. 如果這些字段的名稱不同,先將它們改名茧跋,使每一個(gè)名字都和你想為父類字段取的名字相同慰丛。
  3. 在父類中新建一個(gè)字段,并移除子類中的字段瘾杭。
    4.編譯诅病,測(cè)試。

5.2 Pull Up Method(函數(shù)上移)

有些函數(shù)粥烁,在各個(gè)子類中產(chǎn)生完全相同的結(jié)果贤笆。將該函數(shù)移至超類中。

Pull Up Method

動(dòng)機(jī)

如果函數(shù)在各個(gè)子類中的函數(shù)體都相同讨阻,這就是最顯而易見的適用場(chǎng)合芥永。
Pull Up Mehod常常緊隨其它重構(gòu)而被使用。也許你能找出若干個(gè)身處不同子類中的函數(shù)钝吮,而它們又可以通過其它重構(gòu)手法調(diào)整成為相同的函數(shù)埋涧。這時(shí)候,你就可以先調(diào)整函數(shù)奇瘦,然后再將它們上移到父類中去棘催。當(dāng)然,如果你足夠自信耳标,也可以一次完成這兩個(gè)步驟醇坝。

做法

  1. 檢查待上移的函數(shù),確定它們是完全一致的次坡。
  2. 如果待上移的函數(shù)的名字不同呼猪,先使用Rename Method將它們都修改為你想要在父類中使用的名字画畅。
  3. 在父類中新建一個(gè)函數(shù),將某一個(gè)待上移函數(shù)的代碼復(fù)制到其中郑叠,做適當(dāng)?shù)恼{(diào)整夜赵,然后編譯。
  4. 移除一個(gè)待上移的子類函數(shù)乡革, 編譯寇僧,測(cè)試。
  5. 逐一移除待上移的子類函數(shù)沸版,直到只剩下父類中的函數(shù)為止嘁傀。每次移除之后都需要測(cè)試。
  6. 觀察該函數(shù)的調(diào)用者视粮,看看是否可以改為使用父類類型的對(duì)象细办。

5.2 Pull Up Constructor Body(構(gòu)造函數(shù)本體上移)

在父類中新建一個(gè)構(gòu)造函數(shù),并在子類構(gòu)造函數(shù)中調(diào)用它
重構(gòu)前:

class Manager: Employee {
    init(name: String, id: String, grade: Int) {
        self.name = name
        self.id = id
        self.grade = grade
    }
}

重構(gòu)后:

class Employee {
    init(name: String, id: String) {
        self.name = name
        self.id = id
    }
    ...
}

class Manager: Employee {
    init(name: String, id: String, grade: Int) {
        self.grade = grade
        super.init(name: name, id: id)
    }
    ...
}

動(dòng)機(jī)

如果你看到各個(gè)子類中的函數(shù)有共同行為蕾殴,第一個(gè)念頭就是將共同行為提煉到一個(gè)獨(dú)立函數(shù)中笑撞,然后將這個(gè)函數(shù)上移到父類。對(duì)于構(gòu)造函數(shù)而言钓觉,它們彼此的共同行為往往就是“對(duì)象的構(gòu)建”茴肥。

做法

  1. 在父類中定義一個(gè)構(gòu)造函數(shù)。
  2. 將子類構(gòu)造函數(shù)中的共同代碼搬移到父類構(gòu)造函數(shù)中荡灾。
  3. 將子類構(gòu)造函數(shù)中的共同代碼刪掉瓤狐,改成調(diào)用新建的父類構(gòu)造函數(shù)。
  4. 編譯批幌,測(cè)試础锐。

5.2 Pull Down Method(函數(shù)下移)

父類中的某個(gè)函數(shù)只與部分子類有關(guān),將這個(gè)函數(shù)移到相關(guān)子類去

Pull Down Method

動(dòng)機(jī)

Pull Down Method與Pull Up Method剛好相反荧缘。當(dāng)我們有必要把某些行為從父類移至子類時(shí)皆警,就使用Pull Down Method。使用Extract SubClass之后你可能會(huì)需要它胜宇。

做法

  1. 在所有子類中聲明該函數(shù)耀怜,將父類中的函數(shù)本體復(fù)制到每一個(gè)子類函數(shù)中。
  2. 刪除父類中的函數(shù)桐愉。
  3. 編譯财破,測(cè)試。
  4. 將該函數(shù)從所有不需要它的那些子類中刪掉从诲。
  5. 編譯左痢,測(cè)試。

5.2 Pull Down Field(字段下移)

父類中的某個(gè)字段只被部分子類用到,將這個(gè)字段移到需要它的那些子類中去

Pull Down Field

動(dòng)機(jī)

與Pull Up Field剛好相反俊性。如果只有部分子類需要父類中的一個(gè)字段略步,可以使用本項(xiàng)重構(gòu)。

做法

  1. 在所有子類中聲明該字段定页。
  2. 將該字段從父類中移除趟薄。
  3. 編譯,測(cè)試典徊。
  4. 將該字段從所有不需要它的那些子類中刪掉杭煎。
  5. 編譯,測(cè)試卒落。

5.2 Extract Subclass(提煉子類)

類中的某些特性只被某些實(shí)例用到羡铲,新建一個(gè)子類,將那一部分特性移到子類中

Extract Subclass

動(dòng)機(jī)

與Pull Up Field剛好相反儡毕。如果只有部分子類需要父類中的一個(gè)字段也切,可以使用本項(xiàng)重構(gòu)。

做法

  1. 在所有子類中聲明該字段腰湾。
  2. 將該字段從父類中移除雷恃。
  3. 編譯,測(cè)試费坊。
  4. 將該字段從所有不需要它的那些子類中刪掉褂萧。
  5. 編譯,測(cè)試葵萎。

5.2 Extract Superclass(提煉父類)

兩個(gè)類有相似的特性。為兩個(gè)類建立一個(gè)父類唱凯,將相同特性移至父類羡忘。

Extract Superclass

動(dòng)機(jī)

重復(fù)代碼是系統(tǒng)中最糟糕的東西之一。如果你在不同的地方做同一件事情磕昼,一旦需要修改那些動(dòng)作卷雕,你就需要修改每一份代碼。
如果兩個(gè)類以相同的方式做類似的事情票从,你就需要使用繼承機(jī)制來去除這些重復(fù)漫雕。

做法

  1. 為原本的類創(chuàng)建一個(gè)空白父類。
  2. 運(yùn)用Pull Up Filed峰鄙、Pull Up Method浸间、 Pull Up Constructor Body逐一將子類中的共同元素上移到父類。
  3. 每次上移后吟榴,編譯并測(cè)試魁蒜。
  4. 檢查留在子類中的函數(shù),看它們是否還有共通成分。如果有兜看,可以使用Extract Method將共同部分提煉出來锥咸,然后使用Pull Up Method將提煉出的函數(shù)上移到父類中。
  5. 將所有共通元素上移到父類后细移,檢查子類的所有用戶搏予。如果它們只使用了共同接口,你就可以把調(diào)用它們的對(duì)象類型改為父類類型弧轧。

5.2 Replace Inheritance with Delegation(以委托代替繼承)

在子類中新建一個(gè)字段持有父類的對(duì)象雪侥;調(diào)整子類函數(shù),令它改而委托父類劣针;然后去掉兩者的繼承關(guān)系

Replace Inheritance with Delegation

動(dòng)機(jī)

設(shè)計(jì)模式中講到校镐,優(yōu)先使用組合而不是繼承。
但是你常常會(huì)遇到這樣的情況:一開始繼承了一個(gè)類捺典,隨后發(fā)現(xiàn)父類中的許多操作并不真正適用于子類鸟廓。這是你可以使用組合來代替繼承。

做法

  1. 在子類中新建一個(gè)字段襟己,使其引用父類的一個(gè)對(duì)象引谜,并將它初始化為self。
  2. 修改子類中的所有函數(shù)擎浴,讓它們不再使用父類员咽,轉(zhuǎn)而使用上面新加的那個(gè)字段。
  3. 去除兩個(gè)類之間的繼承關(guān)系贮预,新建一個(gè)受托類的對(duì)象賦給受托字段贝室。
  4. 對(duì)于父類中的每一個(gè)函數(shù),在子類中添加一個(gè)簡(jiǎn)單的委托函數(shù)仿吞。
  5. 編譯滑频,測(cè)試。

5.2 Extract Class(提煉類)

某個(gè)類做了應(yīng)該由兩個(gè)類做的事唤冈。建一個(gè)新類峡迷,將相關(guān)字段和和函數(shù)搬移到新類。

Extract Class

動(dòng)機(jī)

單一職責(zé)原則說到你虹,一個(gè)類應(yīng)該只有一個(gè)職責(zé)绘搞。
如果一個(gè)類職責(zé)過于多,過于復(fù)雜傅物,就應(yīng)該把它們分離到一個(gè)單獨(dú)的類中夯辖。

做法

  1. 決定如果分解類所負(fù)的責(zé)任。
  2. 建立一個(gè)新類挟伙,用以表現(xiàn)從舊類中分離出的職責(zé)楼雹。
  3. 建立從舊類訪問新類的連接關(guān)系模孩。(大部分情況是讓舊類持有新類類型的變量)
  4. 對(duì)于每一個(gè)你想搬移的字段,使用Move Filed進(jìn)行搬移贮缅。
  5. 每次搬移后編譯榨咐,測(cè)試。
  6. 使用Move Method將必要函數(shù)搬移至新類谴供。先搬較低層的函數(shù)块茁,后搬較高層的函數(shù)。
  7. 每次搬移后編譯桂肌,測(cè)試数焊。
  8. 檢查,精簡(jiǎn)每個(gè)類的接口崎场。
  9. 決定是否公開新類佩耳。如果你確實(shí)需要公開它,就需要決定讓它成為引用類型還是值類型谭跨。

六干厚、重構(gòu)示例(心動(dòng)日常查看距離功能重構(gòu))

心動(dòng)日常iOS的查看距離會(huì)根據(jù)用戶所處的位置使用合適的地圖,如果雙方都在國(guó)內(nèi)使用高德地圖螃宙,如果有一方在國(guó)外使用蘋果地圖蛮瞄。
舊版本雙方都有位置信息時(shí)才會(huì)展示地圖,新版本接到新的需求:默認(rèn)展示地圖谆扎,只要一方有位置信息在地圖上對(duì)應(yīng)的位置展示人物頭像挂捅,人物頭像上增加用戶位置權(quán)限標(biāo)識(shí)。

接到需求后堂湖,我先熟悉代碼闲先。經(jīng)過一段時(shí)間的熟悉,我發(fā)現(xiàn)了一些問題:
1.控制器類持有MAMapView和YLAppleMapView類的對(duì)象无蜂,他們的功能類似并且是互斥的饵蒂,可以抽象出相同的接口。
2.一些實(shí)例變量和函數(shù)很明顯應(yīng)該封裝到地圖類內(nèi)部酱讶,而不是放在ViewController類里。
3.需要調(diào)用對(duì)應(yīng)地圖類對(duì)象時(shí)彼乌,都要使用if語(yǔ)句來判斷

因?yàn)檫@些問題泻肯,如果我要修改現(xiàn)有代碼,我必須要修改對(duì)應(yīng)的高德地圖和蘋果的圖部分的代碼慰照。但是因?yàn)樗麄兯诤瘮?shù)的函數(shù)名和具體的業(yè)務(wù)邏輯可能很不一致灶挟,找起來可能會(huì)很麻煩。
如果我要往地圖上添加頭像毒租,需要判斷當(dāng)前是什么地圖稚铣,并分別調(diào)用不同的方法。想想就很頭大,于是我決定重構(gòu)惕医。

重構(gòu)前后代碼結(jié)構(gòu)對(duì)比:

UML圖-重構(gòu)前

UML圖-重構(gòu)后
ViewController成員-重構(gòu)前

ViewController成員-重構(gòu)后
地圖相關(guān)的成員變量-重構(gòu)前

地圖相關(guān)的成員變量-重構(gòu)后

具體重構(gòu)過程通過git代碼演示耕漱。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市抬伺,隨后出現(xiàn)的幾起案子螟够,更是在濱河造成了極大的恐慌,老刑警劉巖峡钓,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件妓笙,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡能岩,警方通過查閱死者的電腦和手機(jī)寞宫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拉鹃,“玉大人辈赋,你說我怎么就攤上這事∶危” “怎么了炭庙?”我有些...
    開封第一講書人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)煌寇。 經(jīng)常有香客問我焕蹄,道長(zhǎng),這世上最難降的妖魔是什么阀溶? 我笑而不...
    開封第一講書人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任腻脏,我火速辦了婚禮,結(jié)果婚禮上银锻,老公的妹妹穿的比我還像新娘永品。我一直安慰自己,他們只是感情好击纬,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開白布鼎姐。 她就那樣靜靜地躺著,像睡著了一般更振。 火紅的嫁衣襯著肌膚如雪炕桨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評(píng)論 1 308
  • 那天肯腕,我揣著相機(jī)與錄音献宫,去河邊找鬼。 笑死实撒,一個(gè)胖子當(dāng)著我的面吹牛姊途,可吹牛的內(nèi)容都是我干的涉瘾。 我是一名探鬼主播,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼捷兰,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼立叛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起寂殉,我...
    開封第一講書人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤囚巴,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后友扰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體彤叉,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年村怪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了秽浇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡甚负,死狀恐怖柬焕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情梭域,我是刑警寧澤斑举,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站病涨,受9級(jí)特大地震影響富玷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜既穆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一赎懦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧幻工,春花似錦励两、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至踢代,卻和暖如春先鱼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背奸鬓。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留掸读,地道東北人串远。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓宏多,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親澡罚。 傳聞我的和親對(duì)象是個(gè)殘疾皇子伸但,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容