這篇博客談一下在實際項目中咱們如何執行重構。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
方法。這個方法有兩個問題:
重構完成後調用的地方是這樣的:
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 有變化,到各個負責具體繪製的方法中修改也不會影響到其餘模塊。
重構的指導思想是什麼?按照一種邏輯整理劃分代碼,把每塊代碼的體量控制在一個容易理解的範圍裏。