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