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

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

首先我們明確一下重構(gòu)的目標(biāo)是什么?重構(gòu)是為了讓項目中的代碼易懂,易維護(hù)。我覺得有一些像家居中的收納。假設(shè)你有一個抽屜,現(xiàn)在你只有一樣?xùn)|西。那么需要去整理收納嗎?其實(shí)意義不大,因為任何人只要打開抽屜,就能知道里面裝了什么。但是隨著業(yè)務(wù)需求的增長,抽屜里的東西越來越多,往里面放東西的人也越來越多。終于過了一個臨界點(diǎn),任何一個人要往抽屜里找東西都越來越難。
所以我們需要保持秩序。這是收納,也是重構(gòu)。

image

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

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 就是太長,大概有四十行。通常情況下一個方法長度超過 20 行意味著做了太多事。當(dāng)然也有一些情況方法長一點(diǎn)是可以接受的。假設(shè)我們有一個抽屜,抽屜裝的都是同一樣?xùn)|西,雖然把抽屜裝滿了,但是對于這個抽屜里裝了什么還是一目了然。如果方法長,但是方法里只是單一的做類似的、很容易理解的事也可以接受。

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

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

那么我們首先做一個小改動,移除對 polygonEditPointViews 的依賴。可以修改方法的參數(shù),把頂點(diǎn)坐標(biāo)當(dāng)做參數(shù)傳進(jìn)來。如果類的規(guī)模小,直接封裝一個屬性提供頂點(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),讀的時候不要去關(guān)心 polygonEditPointViews。

這段代碼主要做了三件事:繪制多邊形,在多邊形邊的中點(diǎn)顯示邊距,在邊上添加增加點(diǎn)的按鈕。實(shí)現(xiàn)的時候三件事的實(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方法。這個方法有兩個問題:

  • 方法的名字不恰當(dāng)。這個方法的作用是創(chuàng)建了一個 layer,并沒有 draw 這個動作。因此名字要修改,以免引起歧義。
  • 方法的參數(shù)不夠好,將參數(shù)的處理細(xì)節(jié)暴露在了外面。這個方法被調(diào)用的地方只有一處,參數(shù)應(yīng)該讓調(diào)用的地方盡量簡潔。字符格式的配置應(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 行。在這個體量下前面說到舊代碼問題的第二點(diǎn)就比較明顯了:中間的繪制增加點(diǎn)的按鈕和其他的層次不同,繪制增加點(diǎn)直接把實(shí)現(xiàn)寫在這里了,抽象層次直接降低了。一個頂層方法應(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 行代碼,這個方法做了什么應(yīng)該非常清晰易理解了。子方法中負(fù)責(zé)各自繪制的部分。如果后期要繪制其他元素,在 drawPolygon 中增加。如果元素的 UI 有變化,到各個負(fù)責(zé)具體繪制的方法中修改也不會影響到其他模塊。

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容