一段代碼的重構實踐記錄

這篇博客談一下在實際項目中咱們如何執行重構。react

首先咱們明確一下重構的目標是什麼?重構是爲了讓項目中的代碼易懂,易維護。我以爲有一些像家居中的收納。假設你有一個抽屜,如今你只有同樣東西。那麼須要去整理收納嗎?其實意義不大,由於任何人只要打開抽屜,就能知道里面裝了什麼。可是隨着業務需求的增加,抽屜裏的東西愈來愈多,往裏面放東西的人也愈來愈多。終於過了一個臨界點,任何一我的要往抽屜裏找東西都愈來愈難。swift

因此咱們須要保持秩序。這是收納,也是重構。app

下面以我在重構自定義地圖控件中項目裏看到的一段代碼爲例,來講明一下重構如何執行。 首先介紹一下需求:在地圖上咱們要繪製一個多邊形,多邊形的頂點須要支持拖動,每次頂點被拖動後,多邊形區域就須要從新繪製。爲了讓用戶在編輯區域的時候更加友好,在編輯時咱們還會展現每條邊的邊長。

下面的代碼的做用就是繪製多邊形。

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)
        
        // 添加距離標記
        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 行意味着作了太多事。固然也有一些狀況方法長一點是能夠接受的。假設咱們有一個抽屜,抽屜裝的都是同同樣東西,雖然把抽屜裝滿了,可是對於這個抽屜裏裝了什麼仍是一目瞭然。若是方法長,可是方法裏只是單一的作相似的、很容易理解的事也能夠接受。post

上面代碼第二個問題是代碼中的抽象層次不一致。我舉個例子,假設公司的 CEO 作了一個決策,他打算通知全部高管,而後高管再逐級同步給部門。可是 CEO 在通知完高管後,詢問高管,這個決策你要通知的人有誰。高管說要通知 A、B、C。因而 CEO 在高管會上把 A、B、C 叫來告訴了他們這個決策。代碼的抽象層級也是相似,原本在處理頂層的邏輯,接着代碼直接去處理了下一層的細節。這樣不一樣層級的代碼在一個方法裏會加大理解的難度。 如今咱們開始一步步重構這段代碼。測試

若是你們看了前面幾篇地圖的控件設計實現的文章,會發現這個方法還有一個結構上的問題。多邊形的頂點位置是從 polygonEditPointViews 上取的。可是若是仔細思考一下,其實這個方法依賴的是頂點的位置,如今經過依賴 polygonEditPointViews 間接獲得,這樣多了沒必要要的依賴。多了這層沒必要要的依賴會增長代碼的不穩定性,另外若是要隔離測試這個方法,隔離的代價也會更高。ui

那麼咱們首先作一個小改動,移除對 polygonEditPointViews 的依賴。能夠修改方法的參數,把頂點座標當作參數傳進來。若是類的規模小,直接封裝一個屬性提供頂點座標也能夠。這裏我選擇比較直觀的封裝屬性方式隔離。spa

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)
            
       		// ... 
            }
        }
		// ...
	}
}
複製代碼

這樣代碼的可讀性也好了一點,讀的時候不要去關心 polygonEditPointViews。設計

這段代碼主要作了三件事:繪製多邊形,在多邊形邊的中點顯示邊距,在邊上添加增長點的按鈕。實現的時候三件事的實現細節又寫在了一塊兒。所以讀起來感受代碼有多有亂。code

咱們首先隔離繪製多邊形的代碼。orm

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)
            }
        }
        // 添加距離標記
        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)
    }
複製代碼

把繪製多邊形的代碼抽離出來後邏輯已經清晰不少了。

接着咱們先重構一下 drawDistanceMarkerLayer方法。這個方法有兩個問題:

  • 方法的名字不恰當。這個方法的做用是建立了一個 layer,並無 draw 這個動做。所以名字要修改,以避免引發歧義。
  • 方法的參數不夠好,將參數的處理細節暴露在了外面。這個方法被調用的地方只有一處,參數應該讓調用的地方儘可能簡潔。字符格式的配置應該在方法內完成。

重構完成後調用的地方是這樣的:

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

 //原來的調用
 let markerLayer = drawDistanceMarkerLayer(centerPoint: editPoint, text: String(format: "%.0f", markerDistance) + "m")
複製代碼

接着咱們把距離標記再抽出來。

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)
        }
        // 添加距離標記
        distanceMarkerLayer.sublayers?.removeAll()
        distanceMarkerLayer.removeFromSuperlayer()
        distanceMarkerLayers.forEach {
            distanceMarkerLayer.addSublayer($0)
        }
        distanceMarkerLayer.zPosition -= 1
        layer.addSublayer(distanceMarkerLayer)
    }
複製代碼

作完這一步 drawPolygon 裏的代碼行數已經不多了,只有不到 10 行。在這個體量下前面說到舊代碼問題的第二點就比較明顯了:中間的繪製增長點的按鈕和其餘的層次不一樣,繪製增長點直接把實現寫在這裏了,抽象層次直接下降了。一個頂層方法應該負責調度,細節的實現不該該在裏面。

最後咱們把繪製增長點的按鈕抽離出來。

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 行代碼,這個方法作了什麼應該很是清晰易理解了。子方法中負責各自繪製的部分。若是後期要繪製其餘元素,在 drawPolygon 中增長。若是元素的 UI 有變化,到各個負責具體繪製的方法中修改也不會影響到其餘模塊。

重構的指導思想是什麼?按照一種邏輯整理劃分代碼,把每塊代碼的體量控制在一個容易理解的範圍裏。

相關文章
相關標籤/搜索