一段代碼的重構(gòu)實(shí)踐記錄

這篇博客談一下在實(shí)際項(xiàng)目中我們?nèi)绾螆?zhí)行重構(gòu)。

首先我們明確一下重構(gòu)的目標(biāo)是什么叠殷?重構(gòu)是為了讓項(xiàng)目中的代碼易懂雹洗,易維護(hù)。我覺得有一些像家居中的收納牛欢。假設(shè)你有一個(gè)抽屜,現(xiàn)在你只有一樣?xùn)|西淆游。那么需要去整理收納嗎傍睹?其實(shí)意義不大,因?yàn)槿魏稳酥灰蜷_抽屜犹菱,就能知道里面裝了什么拾稳。但是隨著業(yè)務(wù)需求的增長(zhǎng),抽屜里的東西越來越多腊脱,往里面放東西的人也越來越多访得。終于過了一個(gè)臨界點(diǎn),任何一個(gè)人要往抽屜里找東西都越來越難虑椎。
所以我們需要保持秩序震鹉。這是收納,也是重構(gòu)捆姜。

image

下面以我在重構(gòu)自定義地圖控件中項(xiàng)目里看到的一段代碼為例传趾,來說明一下重構(gòu)如何執(zhí)行。
首先介紹一下需求:在地圖上我們要繪制一個(gè)多邊形泥技,多邊形的頂點(diǎn)需要支持拖動(dòng)浆兰,每次頂點(diǎn)被拖動(dòng)后,多邊形區(qū)域就需要重新繪制珊豹。為了讓用戶在編輯區(qū)域的時(shí)候更加友好簸呈,在編輯時(shí)我們還會(huì)展示每條邊的邊長(zhǎng)。

image

下面的代碼的作用就是繪制多邊形店茶。

class CustomMapOverlayView: UIView {
    var polygonEditPointViews = [PolygonAnnotationView]()
    var polygonLayer = CAShapeLayer()
    var distanceMarkerLayer = CAShapeLayer()

    private func drawPolygon(currentIndex: Int = 0, showDistanceMarker: Bool = true) {
        guard polygonControlPointViews.count >= 3 else { return }
        polygonEditPointViews.forEach { $0.removeFromSuperview() }
        polygonEditPointViews.removeAll()
        var distanceMarkerLayers = [CAShapeLayer]()
        let polygonPath = UIBezierPath()
        
        for (index, polygonControlPointView) in polygonControlPointViews.enumerated() {
            if index == 0 {
                polygonPath.move(to: polygonControlPointView.center)
            }
            let nextIndex = (index + 1) % polygonControlPointViews.count
            let nextControlPoint = polygonControlPointViews[nextIndex].center
            polygonPath.addLine(to: nextControlPoint)

            let editPoint = GeometryHelper.getMiddlePoint(point1: polygonControlPointView.center, point2: polygonControlPointViews[nextIndex].center)
            addPolygonPointView(center: editPoint, type: .add)
            
            if showDistanceMarker {
                let nextCoordinate = currentPolygonVertexes[nextIndex].coordinate
                let markerDistance = GeometryHelper.getDistance(from: currentPolygonVertexes[index].coordinate, to: nextCoordinate).rounded()
                let markerLayer = drawDistanceMarkerLayer(centerPoint: editPoint, text: String(format: "%.0f", markerDistance) + "m")
                distanceMarkerLayers.append(markerLayer)
            }
        }
        polygonPath.close()
        
        polygonLayer.removeFromSuperlayer()
        polygonLayer.path = polygonPath.cgPath
        polygonLayer.lineWidth = 1
        // 判斷多邊形是否合法蜕便,不合法則將線段以紅色顯示
        polygonLayer.strokeColor = isPolygonValid(index: currentIndex) ? polygonColor.cgColor : UIColor.red.cgColor
        polygonLayer.fillColor = polygonColor.cgColor
        polygonLayer.zPosition -= 1
        layer.addSublayer(polygonLayer)
        
        // 添加距離標(biāo)記
        distanceMarkerLayer.sublayers?.removeAll()
        distanceMarkerLayer.removeFromSuperlayer()
        distanceMarkerLayers.forEach {
            distanceMarkerLayer.addSublayer($0)
        }
        distanceMarkerLayer.zPosition -= 1
        layer.addSublayer(distanceMarkerLayer)
    }

    private func drawDistanceMarkerLayer(centerPoint: CGPoint, text: String) -> CAShapeLayer {
        let textSize = getTextSize(text: text)
        let react = CGRect(x: centerPoint.x - 8, y: centerPoint.y - 8, width: textSize.width + 24, height: 16)
        let roundRectPath = UIBezierPath(roundedRect: react, cornerRadius: 8)
        let markerLayer = CAShapeLayer()
        markerLayer.path = roundRectPath.cgPath
        markerLayer.fillColor = UIColor.white.cgColor
        
        let textLayer = drawTextLayer(frame: CGRect(x: react.origin.x + 18, y: react.origin.y + (8 - textSize.height/2), width: textSize.width, height: textSize.height), text: text, foregroundColor: MeshColor.grey2, backgroundColor: UIColor.clear)
        markerLayer.addSublayer(textLayer)
        return markerLayer
    }
}

上面這段代碼非常明顯的 bad smell 就是太長(zhǎng),大概有四十行贩幻。通常情況下一個(gè)方法長(zhǎng)度超過 20 行意味著做了太多事轿腺。當(dāng)然也有一些情況方法長(zhǎng)一點(diǎn)是可以接受的两嘴。假設(shè)我們有一個(gè)抽屜,抽屜裝的都是同一樣?xùn)|西族壳,雖然把抽屜裝滿了憔辫,但是對(duì)于這個(gè)抽屜里裝了什么還是一目了然。如果方法長(zhǎng)仿荆,但是方法里只是單一的做類似的贰您、很容易理解的事也可以接受。

上面代碼第二個(gè)問題是代碼中的抽象層次不一致拢操。我舉個(gè)例子锦亦,假設(shè)公司的 CEO 做了一個(gè)決策,他打算通知所有高管令境,然后高管再逐級(jí)同步給部門孽亲。但是 CEO 在通知完高管后,詢問高管展父,這個(gè)決策你要通知的人有誰(shuí)。高管說要通知 A玲昧、B栖茉、C。于是 CEO 在高管會(huì)上把 A孵延、B吕漂、C 叫來告訴了他們這個(gè)決策。代碼的抽象層級(jí)也是類似尘应,本來在處理頂層的邏輯惶凝,接著代碼直接去處理了下一層的細(xì)節(jié)。這樣不同層級(jí)的代碼在一個(gè)方法里會(huì)加大理解的難度犬钢。
現(xiàn)在我們開始一步步重構(gòu)這段代碼苍鲜。

如果大家看了前面幾篇地圖的控件設(shè)計(jì)實(shí)現(xiàn)的文章,會(huì)發(fā)現(xiàn)這個(gè)方法還有一個(gè)結(jié)構(gòu)上的問題玷犹。多邊形的頂點(diǎn)位置是從 polygonEditPointViews 上取的混滔。但是如果仔細(xì)思考一下,其實(shí)這個(gè)方法依賴的是頂點(diǎn)的位置歹颓,現(xiàn)在通過依賴 polygonEditPointViews 間接得到坯屿,這樣多了不必要的依賴。多了這層不必要的依賴會(huì)增加代碼的不穩(wěn)定性巍扛,另外如果要隔離測(cè)試這個(gè)方法领跛,隔離的代價(jià)也會(huì)更高。

那么我們首先做一個(gè)小改動(dòng)撤奸,移除對(duì) polygonEditPointViews 的依賴吠昭『袄ǎ可以修改方法的參數(shù),把頂點(diǎn)坐標(biāo)當(dāng)做參數(shù)傳進(jìn)來怎诫。如果類的規(guī)模小瘾晃,直接封裝一個(gè)屬性提供頂點(diǎn)坐標(biāo)也可以。這里我選擇比較直觀的封裝屬性方式隔離幻妓。

class CustomMapOverlayView: UIView {  
   var polygonEditPointViews = [PolygonAnnotationView]()
   private var areaVertexs: [CGPoint] {
        return polygonControlPointViews.map { $0.center }
   }

    private func drawPolygon(currentIndex: Int = 0, showDistanceMarker: Bool = true) {
        guard areaVertexs.count >= 3 else { return }
        polygonEditPointViews.forEach { $0.removeFromSuperview() }
        polygonEditPointViews.removeAll()
        var distanceMarkerLayers = [CAShapeLayer]()
        let polygonPath = UIBezierPath()
        
        for (index, vertex) in areaVertexs.enumerated() {
            if index == 0 {
                polygonPath.move(to: vertex)
            }
            let nextIndex = (index + 1) % areaVertexs.count
            let nextControlPoint = areaVertexs[nextIndex]
            polygonPath.addLine(to: nextControlPoint)

            let editPoint = GeometryHelper.getMiddlePoint(point1: vertex, point2: areaVertexs[nextIndex])
            addPolygonPointView(center: editPoint, type: .add)
            
            // ...  
            }
        }
        // ...
    }
}

這樣代碼的可讀性也好了一點(diǎn)蹦误,讀的時(shí)候不要去關(guān)心 polygonEditPointViews肉津。

這段代碼主要做了三件事:繪制多邊形强胰,在多邊形邊的中點(diǎn)顯示邊距,在邊上添加增加點(diǎn)的按鈕妹沙。實(shí)現(xiàn)的時(shí)候三件事的實(shí)現(xiàn)細(xì)節(jié)又寫在了一起偶洋。因此讀起來感覺代碼有多有亂。

我們首先隔離繪制多邊形的代碼距糖。

     var polygonLayer = CAShapeLayer()
   
     private func drawPolygon(currentIndex: Int = 0, showDistanceMarker: Bool = true) {
        guard areaVertexs.count >= 3 else { return }
        renderPolygonLayer(changedPointIndex: currentIndex)
        polygonEditPointViews.forEach { $0.removeFromSuperview() }
        polygonEditPointViews.removeAll()
        var distanceMarkerLayers = [CAShapeLayer]()
        for (index, vertex) in areaVertexs.enumerated() {
            let nextIndex = (index + 1) % areaVertexs.count
            let editPoint = GeometryHelper.getMiddlePoint(point1: vertex, point2: areaVertexs[nextIndex])
            addPolygonPointView(center: editPoint, type: .add)
            if showDistanceMarker {
                let nextCoordinate = currentPolygonVertexes[nextIndex].coordinate
                let markerDistance = GeometryHelper.getDistance(from: currentPolygonVertexes[index].coordinate, to: nextCoordinate).rounded()
                let markerLayer = drawDistanceMarkerLayer(centerPoint: editPoint, text: String(format: "%.0f", markerDistance) + "m")
                distanceMarkerLayers.append(markerLayer)
            }
        }
        // 添加距離標(biāo)記
        distanceMarkerLayer.sublayers?.removeAll()
        distanceMarkerLayer.removeFromSuperlayer()
        distanceMarkerLayers.forEach {
            distanceMarkerLayer.addSublayer($0)
        }
        distanceMarkerLayer.zPosition -= 1
        layer.addSublayer(distanceMarkerLayer)
    }
    
    private func renderPolygonLayer(changedPointIndex: Int = 0) {
        let polygonPath = UIBezierPath()
        polygonPath.move(to: areaVertexs[0])
        for index in 1 ..< areaVertexs.count {
            let nextIndex = (index + 1) % areaVertexs.count
            let nextControlPoint = areaVertexs[nextIndex]
            polygonPath.addLine(to: nextControlPoint)
        }
        polygonPath.close()

        polygonLayer.removeFromSuperlayer()
        polygonLayer.path = polygonPath.cgPath
        polygonLayer.lineWidth = 1
        // 判斷多邊形是否合法玄窝,不合法則將線段以紅色顯示
        polygonLayer.strokeColor = isPolygonValid(index: changedPointIndex) ? polygonColor.cgColor : UIColor.red.cgColor
        polygonLayer.fillColor = polygonColor.cgColor
        polygonLayer.zPosition -= 1
        layer.addSublayer(polygonLayer)
    }

把繪制多邊形的代碼抽離出來后邏輯已經(jīng)清晰很多了。

接著我們先重構(gòu)一下 drawDistanceMarkerLayer方法悍引。這個(gè)方法有兩個(gè)問題:

  • 方法的名字不恰當(dāng)恩脂。這個(gè)方法的作用是創(chuàng)建了一個(gè) layer,并沒有 draw 這個(gè)動(dòng)作趣斤。因此名字要修改俩块,以免引起歧義。
  • 方法的參數(shù)不夠好浓领,將參數(shù)的處理細(xì)節(jié)暴露在了外面玉凯。這個(gè)方法被調(diào)用的地方只有一處,參數(shù)應(yīng)該讓調(diào)用的地方盡量簡(jiǎn)潔联贩。字符格式的配置應(yīng)該在方法內(nèi)完成漫仆。

重構(gòu)完成后調(diào)用的地方是這樣的:

 let markerLayer = createDistanceMarkerLayer(centerPoint: editPoint, markerDistance: markerDistance)

    //原來的調(diào)用
 let markerLayer = drawDistanceMarkerLayer(centerPoint: editPoint, text: String(format: "%.0f", markerDistance) + "m")

接著我們把距離標(biāo)記再抽出來。

    private func drawPolygon(currentIndex: Int = 0, showDistanceMarker: Bool = true) {
        guard areaVertexs.count >= 3 else { return }
        renderPolygonLayer(changedPointIndex: currentIndex)
        polygonEditPointViews.forEach { $0.removeFromSuperview() }
        polygonEditPointViews.removeAll()
        for (index, vertex) in areaVertexs.enumerated() {
            let nextIndex = (index + 1) % areaVertexs.count
            let editPoint = GeometryHelper.getMiddlePoint(point1: vertex, point2: areaVertexs[nextIndex])
            addPolygonPointView(center: editPoint, type: .add)
        }
        if showDistanceMarker {
            renderDistanceMarkerLayer()
        }
    }
    
    private func renderDistanceMarkerLayer() {
        var distanceMarkerLayers = [CAShapeLayer]()
        for index in 0 ..< areaVertexs.count {
            let nextIndex = (index + 1) % areaVertexs.count
            let middlePoint = GeometryHelper.getMiddlePoint(point1: areaVertexs[index], point2: areaVertexs[nextIndex])
            let nextCoordinate = currentPolygonVertexes[nextIndex].coordinate
            let markerDistance = GeometryHelper.getDistance(from: currentPolygonVertexes[index].coordinate, to: nextCoordinate).rounded()
            let markerLayer = createDistanceMarkerLayer(centerPoint: middlePoint, markerDistance: markerDistance)
            distanceMarkerLayers.append(markerLayer)
        }
        // 添加距離標(biāo)記
        distanceMarkerLayer.sublayers?.removeAll()
        distanceMarkerLayer.removeFromSuperlayer()
        distanceMarkerLayers.forEach {
            distanceMarkerLayer.addSublayer($0)
        }
        distanceMarkerLayer.zPosition -= 1
        layer.addSublayer(distanceMarkerLayer)
    }

做完這一步 drawPolygon 里的代碼行數(shù)已經(jīng)很少了泪幌,只有不到 10 行歹啼。在這個(gè)體量下前面說到舊代碼問題的第二點(diǎn)就比較明顯了:中間的繪制增加點(diǎn)的按鈕和其他的層次不同,繪制增加點(diǎn)直接把實(shí)現(xiàn)寫在這里了座菠,抽象層次直接降低了狸眼。一個(gè)頂層方法應(yīng)該負(fù)責(zé)調(diào)度,細(xì)節(jié)的實(shí)現(xiàn)不應(yīng)該在里面浴滴。

最后我們把繪制增加點(diǎn)的按鈕抽離出來拓萌。

    private func drawPolygon(currentIndex: Int = 0, showDistanceMarker: Bool = true) {
        guard areaVertexs.count >= 3 else { return }
        renderPolygonLayer(changedPointIndex: currentIndex)
        renderEditPoints()
        if showDistanceMarker {
            renderDistanceMarkerLayer()
        }
    }
    
    private func renderEditPoints() {
        polygonEditPointViews.forEach { $0.removeFromSuperview() }
        polygonEditPointViews.removeAll()
        
        for (index, vertex) in areaVertexs.enumerated() {
            let nextIndex = (index + 1) % areaVertexs.count
            let editPoint = GeometryHelper.getMiddlePoint(point1: vertex, point2: areaVertexs[nextIndex])
            let polygonPoint = createPolygonPoint(center: editPoint, type: .add)
            addSubview(polygonPoint)
            polygonEditPointViews.append(polygonPoint)
        }
    }

完成后核心方法 drawPolygon 只有 5 行代碼,這個(gè)方法做了什么應(yīng)該非常清晰易理解了升略。子方法中負(fù)責(zé)各自繪制的部分微王。如果后期要繪制其他元素屡限,在 drawPolygon 中增加。如果元素的 UI 有變化炕倘,到各個(gè)負(fù)責(zé)具體繪制的方法中修改也不會(huì)影響到其他模塊钧大。

重構(gòu)的指導(dǎo)思想是什么?按照一種邏輯整理劃分代碼罩旋,把每塊代碼的體量控制在一個(gè)容易理解的范圍里啊央。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市涨醋,隨后出現(xiàn)的幾起案子瓜饥,更是在濱河造成了極大的恐慌,老刑警劉巖浴骂,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乓土,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡溯警,警方通過查閱死者的電腦和手機(jī)趣苏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來梯轻,“玉大人拦键,你說我怎么就攤上這事¢萘埽” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵萄金,是天一觀的道長(zhǎng)蟀悦。 經(jīng)常有香客問我,道長(zhǎng)氧敢,這世上最難降的妖魔是什么日戈? 我笑而不...
    開封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮孙乖,結(jié)果婚禮上浙炼,老公的妹妹穿的比我還像新娘。我一直安慰自己唯袄,他們只是感情好弯屈,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著恋拷,像睡著了一般资厉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蔬顾,一...
    開封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天宴偿,我揣著相機(jī)與錄音湘捎,去河邊找鬼。 笑死窄刘,一個(gè)胖子當(dāng)著我的面吹牛窥妇,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播娩践,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼活翩,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了欺矫?” 一聲冷哼從身側(cè)響起纱新,我...
    開封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎穆趴,沒想到半個(gè)月后脸爱,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡未妹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年簿废,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片络它。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡族檬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出化戳,到底是詐尸還是另有隱情单料,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布点楼,位于F島的核電站扫尖,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏掠廓。R本人自食惡果不足惜换怖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蟀瞧。 院中可真熱鬧沉颂,春花似錦、人聲如沸悦污。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)切端。三九已至抬探,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背小压。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工线梗, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人怠益。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓仪搔,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親蜻牢。 傳聞我的和親對(duì)象是個(gè)殘疾皇子烤咧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

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