Massive View Controller 重構: Swift Extension 整理術


Extension 是 Swift 裡用來延伸既有型別的東西。透過 Extension,當我們想為某個型別加功能的時候,就可以不用把新的功能寫在該型別的主體裡面。比如說,如果我們想為 Cat 增加一個 purr() 方法的時候,可以這樣寫:

這種能力讓我們可以為一些碰不到原始碼的型別,加上我們自己給的功能。這也就是所謂的回顧式建模 (Retrospective Modeling)── 在不更改原本型別的前提下,去為這個型別增加功能。

雖然這感覺並不是甚麼厲害的設計模式,但其實善加運用的話,就可以大幅簡化 Massive View Controller。以下,我們就一起來看看它有甚麼樣的用法。

本篇所說的「型別」是指 Class、Structure 與 Enumeration 這三種實際型別。關於 Protocol Extension,可以參考 ShihTing Huang 的這一篇文章。)

模組化

Extension 並不只是拿來加功能而已,它也可以用來打散你自己寫的型別。比如說,我們可以把所有的工廠方法都丟到同一個 Extension 裡面:

這樣做的好處,是在閱讀這個型別的程式碼時,我們可以更容易去定位某一個方法。不要小看單純視覺上的分區,當你找 bug 找到焦頭爛額的時候,你會感謝當初自己有把程式碼好好整理過。

你可能會想,這跟用 // MARK: 來做段落有甚麼差別呢?// MARK: 還讓你可以為段落命名勒!其實,這兩種東西並不會互相排斥,我們完全可以將它們搭配使用,像這樣:

如此一來,我們就可以在 Xcode 的 Jump bar 下拉式選單裡,知道這個 Extension 是做甚麼的:

swift-extension-1

同時,Extension 確實也提供了一些 // MARK: 不具有的功能。

程式碼折疊

在最新版的 Xcode 裡,所有被大括號(「{」與「}」)括起來的程式碼都可以被折疊。Extension 即是用大括號把一些程式碼包起來,所以我們可以把整個 Extension 都折疊起來,像這樣:

swift-extension-2

要怎麼折疊呢?除了彼得潘所提到的方法── 使用 Code folding ribbon 之外,也可以:

  1. 將鍵盤輸入游標移動到該 Extension 所處的任何一行。
  2. 按下鍵盤快捷鍵:Command-Option-Left Arrow

如果要展開的話,改按 Command-Option-Right Arrow就可以了。

程式碼折疊對於想專心處理其它部份的程式碼時非常有用,畢竟眼不見為淨。然而除了折疊之外,想隱藏程式碼還有另一種更基進的做法。

移到新檔案

沒錯,把整個 Extension 移到別的檔案去,你就不會再在這裡看到它了。比如說,我們可以新增一個叫做 MyViewController+FactoryMethods.swift 的檔案,並把相關的 Extension 整個剪貼過去:

這在原本的 View Controller 行數太多時尤其有用;但除此之外,新檔案也意味著幾件事。

首先是創造了新的隱私權範圍。不管是 private 還是 fileprivate ,現在 (Swift 4.2) 都是以檔案作為最大範圍的。也就是說,我們得以在原本的 MyViewController.swift 與新的 MyViewController+FactoryMethods.swift 裡,各自定義只能在檔案內部讀取的方法,以此增強個別的模組化,並降低複雜度。

再來是開啟了在不同 Target(編譯目標)之間、不同組合的可能性。由於 Swift 編譯器是以檔案為基本單位的,所以我們可以選擇在某些 Target 裡不要包含某些檔案。比如說,我們可能只有在主程式裡才需要編輯功能,那就可以把所有的編輯功能都透過 Extension 放到一個新檔案裡,並只在主程式 Target 裡包含該檔案。

遵守 Protocol

Extension 可以加的功能並不只是方法與計算型屬性而已,它還可以加上對 Protocol 的遵守。比如說,我們可以使 MyViewController 的 Extension 去遵守 UITableViewDataSource

使用 Extension 來遵守 Protocol 可以說是模組化的最佳實踐,因為這使得我們能馬上就明白哪些方法與屬性是屬於哪個 Protocol 的。這也可以提醒自己把同個 Protocol 的方法寫在一起,使我們不用去煩惱哪個方法要寫在哪一行的同時,也可以寫出有條不紊的程式碼。

另外前面也有提到,我們可以在不同 Target 之間採取不同的檔案組合,以控制某型別功能的差異;但我們並沒有說要怎麼判斷該型別是否有某種功能。雖然我們可以用 Swift 編譯器的 Active Compilation Conditions 編譯符、與 #if/#else/#endif 判斷式,去決定所在的 Target 有沒有包含某個 Extension;但用 Protocol 來判斷的話,就不用去碰編譯設定了。

比如說,假設我們想要把 MyViewController 的編輯功能限縮在主程式之內的話,那我們可以這樣寫:

如此一來,在使用者按下刪除、並觸發 deleteButtonWasPressed() 的時候,Swift 的 Runtime 就會去檢查 MyViewController 是不是 Editable,進而執行編輯指令 setImage(_:)。而由於只有在主程式裡 MyViewController 才遵守 Editable,所以在其它的 Target 裡 setImage(_:) 是不會被執行的。

將程式碼歸類到適合的型別裡

在寫 View Controller 的時候,我們經常會用到各種工廠方法與輔助方法 (Helper Method)。它們可以使程式碼更整齊,也能幫助我們理解某一段程式碼的意義。

比如說,我們要寫一個負責彈出刪除警告視窗的輔助方法。而一如前文提到,我們可以把它放到一個 Extension 裡面去,以方便管理。

透過把樣板程式碼 (boilerplate code) 用一個輔助方法包起來,並收納到 Extension 裡,我們使型別主體的程式碼更容易閱讀了。然而,這還不是最乾淨的方式,因為雖然這個輔助方法已經在 Extension 裡了,但它仍然跟型別主體互相依賴。這個時候,我們就可以想想有沒有更乾淨的寫法。

首先,讓我們來想想這個輔助方法到底做了甚麼事?在這裡,它主要就做了兩件事:創造一個 UIAlertController,以及去呈現它。但其實呈現所需的程式碼也就只有一行而已,所以這個方法最主要的職責,其實就是創造 UIAlertController 的實體。

沒錯,聽起來很像工廠方法。不過,Swift 其實提供了另一種工具來滿足這個需求:Convenience Initializer。也就是說,與其把它改寫成這樣:

不如試試寫成這樣:

為甚麼會有 deletionHandler 這個參數呢?因為原來的寫法裡,刪除的動作會直接去呼叫 self?.deleteContent(),造成對 MyViewController 的依賴。用閉包來取代掉這一行的話,就可以解除依賴了。整個程式碼如下:

現在,型別與型別之間的分工更清楚了!MyViewController 負責在適當時機要 UIAlertController 創造一個刪除警告出來,然後再呈現它;而 UIAlertController 則負責創造這個刪除警告。這就符合了「高聚合,低耦合 (High Cohesion, Low Coupling)」原則,或者簡單說,就是各司其職

另一個潛在的好處,是我們現在也可以在別的地方使用這個 Convenience Initializer 了。由於它沒有對官方框架以外的型別有任何依賴,所以它是可以在任何有 import UIKit 的檔案裡使用。

不過,程式碼的整理並不需要以重用性為目標。我們甚至可以把這個 Convenience Initializer 標記為 fileprivate,以使它只能在同一個檔案裡被使用。我們光是讓程式碼易於理解就已經達到目標了,不必要為了重用性而犧牲易讀性。

還有很棒的一點是:這樣寫可以省下很多字符。如果比較一下輔助方法版本的 presentDeleteConfirmationAlert() 與 Convenience Initializer 版本的 init(deletionHandler:),你會發現後者完全沒有出現 alertController 這個辨識符!因為前者的 alertController 在後者其實就是 self,所以可以整個省略掉。

事實上,這也可以拿來判斷說,把程式碼放到哪個型別的 Extension 會比較好。在一個輔助方法中,如果某個辨識符出現的頻率越高,那它就越可能是這個輔助方法中的主角,我們就可以試著把整個輔助方法移到該辨識符型別的 Extension 裡。

擴展通用型別

Extension 可以只針對滿足了某條件的通用型別 (Generic Type) 增加功能,這適合應用在內建的許多集合型別上,因為集合型別大多都是通用型別。比如說,在使用 UITableViewUICollectionView 的時候,我們常常會需要用 IndexPath 去取值,像是這樣:

可以看到,取個值就要寫一串長長的程式碼,用了兩次下標(Subscript)語法。但既然兩次下標的輸入來源都是同一個 indexPath,我們有沒有辦法簡化這整個陳述呢?

有的,Extension 就可以做到了。

用 Extension 加入上述的下標之後,我們就可以把原本的方法改成這樣:

是不是簡化許多了呢?

結論

軟體重構這件事,其實就是對程式碼做整體的整理。除了大規模的架構更動之外,小規模的分段、分檔案等都是能增加程式碼可讀性的手段,而 Extension 就很適合拿來應用在小規模的重構。有的時候,你甚至會發現有些問題其實不用去寫一個新的型別來處理,只要一個 Extension 就可以解決了。讀完這篇,也希望你得到一些活用 Extension 的靈感!


iOS 開發者、寫作者、filmmaker。現正負責開發 Storyboards by narrativesaw 此一故事板文件 app 中。深深認同 Swift 對於程式碼易讀性的重視。個人網站:lihenghsu.com。電郵:[email protected]

blog comments powered by Disqus
訂閲電子報

訂閲電子報

AppCoda致力於發佈優質iOS程式教學,你不必每天上站,輸入你的電子郵件地址訂閱網站的最新教學文章。每當有新文章發佈,我們會使用電子郵件通知你。

已收你的指示。請你檢查你的電郵,我們已寄出一封認證信,點擊信中鏈結才算完成訂閱。

Shares
Share This