在使用一個物件之前,我們經常會需要對其進行設定。比如說,使用一個 UIView
之前,有時我們會需要指定它的背景色彩等屬性:
class ViewController: UIViewController {
override func loadView() {
// 建構一個 UIView 物件。
let view = UIView()
// 設定 view。
view.backgroundColor = .systemRed
view.layer.cornerRadius = 5
view.layer.masksToBounds = true
// 指派 view 給 self.view。
self.view = view
}
}
這些設定程式碼跟其它的邏輯程式碼是相當不同的存在。它們往往只跟被設定的那個物件有關,像這裡就是只跟 view
有關,跟 ViewController
無關。所以,要重構它們也是相對上簡單的。比如說,我們可以直接把它們包裝成一個 ⋯⋯
自由函數工廠方法
// 工廠方法
private func makeView() -> UIView {
// 建構一個 UIView 物件。
let view = UIView()
// 設定 view。
view.backgroundColor = .systemRed
view.layer.cornerRadius = 5
view.layer.masksToBounds = true
// 回傳 view。
return view
}
class ViewController: UIViewController {
override func loadView() {
// 把 makeView() 所產生的 UIView 物件指派給 self.view。
self.view = makeView()
}
}
我們把工廠方法寫成一個 private
的自由函數,因為這樣可以防止它去存取 ViewController
的任何屬性或方法。而在 ViewController
裡,我們也只需要一行程式碼就可以取用已經設定好的 UIView
物件,使程式碼更易讀。
然而自由函數如果一多,管理起來就會相對困難。首先,是要記得函數名才能呼叫,這還可以靠在命名時以 make-
做工廠方法開頭的慣例來解決。再來,是不同回傳型別的函數,都會在 Xcode 的自動完成列表裡面出現。所以,如果我們有另一個這樣的工廠方法:
private func makeLoadingViewController() -> UIViewController {
// ...
}
那當我們輸入 make
的時候,即使是要產生拿來指派給 self.view
的 UIView
物件,makeView()
跟 makeLoadingViewController
都會同時出現。
所以,雖然自由函數工廠方法是很好的重構與解耦工具,但使用上總是有那麼一點的不方便。還好 Swift 提供了另一種工具,既具備自由函數的解耦性,又能充分運用 Xcode 的自動完成系統,那就是 ⋯⋯
Static Factory Method!
在 Swift 裡,只要在某個方法前面加上 static
關鍵字,就會使它成為是屬於那個型別本身的方法。如果我們把型別當成是製造物件的藍圖的話,那 static method 就等於是藍圖本身的方法。而由於藍圖本身最大的用途就是拿來製造物件,所以它的方法也就常常都是工廠方法,用來補全 initializer 所不能做到的事。
甚麼是 initializer 做不到的呢?
- 命名:Initializer 只能對參數命名,沒辦法對方法本身命名。所以,同一個型別裡沒辦法有兩個不同的沒有參數的 initializer。
- 多型:Initializer 所產生的必然是該型別的物件,不會是它的子類型。比如說我們沒辦法使一個
UIView
的 initializer 回傳一個UIButton
。
延續前面的例子,我們可以把 makeView()
改寫成 UIView
的 static factory method:
extension UIView {
// Static 工廠方法
static func makeView() -> UIView {
// 建構一個 UIView 物件。
let view = UIView()
// 設定 view。
view.backgroundColor = .systemRed
view.layer.cornerRadius = 5
view.layer.masksToBounds = true
// 回傳 view。
return view
}
}
class ViewController: UIViewController {
override func loadView() {
// 把 UIView.makeView() 所產生的 UIView 物件指派給 self.view。
self.view = .makeView()
}
}
如此一來,當我們輸入到 self.view = .
的時候,Xcode 就可以判斷型別,並在自動完成列表裡,列出該型別所有的 initializer,以及正確回傳型別的 static method:
這樣就可以過濾掉所有不相關的函數與方法了。
實戰重構 UIAlertController
UIAlertController
是最容易產生 boilerplate code 的物件之一。即使是一個普通的確認警告,都得花上好幾行程式碼來實作:
class ViewController: UITableViewController {
// 處理使用者動作的方法
@IBAction func deleteButtonPressed(_ sender: UIBarButtonItem) {
// 產生一個 UIAlertController 物件。
let alertController = UIAlertController(title: "刪除警告", message: "你確定要刪除這筆資料嗎?", preferredStyle: .alert)
// 給 alertController 加上刪除的選項。
let deleteAction = UIAlertAction(title: "刪除", style: .destructive) { action in
self.deleteData()
}
alertController.addAction(deleteAction)
// 給 alertController 加上取消的選項。
let cancelAction = UIAlertAction(title: "取消", style: .cancel, handler: nil)
alertController.addAction(cancelAction)
// 呈現 alertController。
present(alertController, animated: true, completion: nil)
}
// 刪除資料用的方法
func deleteData() {
// ...
}
}
幸好,我們有 static factory method 可以用。先把標題、UIAlertAction
等設定程式碼丟到一個 static method 裡,並設一個 handler 屬性來定義按下確認後要執行的動作:
extension UIAlertController {
static func confirmationAlert(title: String?, message: String?, handler: @escaping () -> Void) -> UIAlertController {
// 產生一個 UIAlertController 物件。
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
// 給 alertController 加上確認的選項。
let confirmAction = UIAlertAction(title: "刪除", style: .destructive) { action in
handler()
}
alertController.addAction(confirmAction)
// 給 alertController 加上取消的選項。
let cancelAction = UIAlertAction(title: "取消", style: .cancel, handler: nil)
alertController.addAction(cancelAction)
return alertController
}
}
然後,我們就可以在 ViewController
裡用它來產生 alert controller:
class ViewController: UITableViewController {
// 處理使用者動作的方法
@IBAction func deleteButtonPressed(_ sender: UIBarButtonItem) {
// 產生確認用的 UIAlertController。
let alertController = UIAlertController.confirmationAlert(title: "刪除警告", message: "你確定要刪除這筆資料嗎?") {
self.deleteData()
}
// 呈現 alertController。
present(alertController, animated: true, completion: nil)
}
// 刪除資料用的方法
func deleteData() {
// ...
}
}
將原本繁複的程式碼縮減成這樣已經很棒了,但我們並沒有發揮到 Xcode 的自動完成功能。要怎麼做才有效呢?很簡單,把 static factory method 改定義在 UIViewController
底下就好了:
// 從 UIAlertController 改成 UIViewController
extension UIViewController {
static func confirmationAlert(title: String?, message: String?, handler: @escaping () -> Void) -> UIAlertController {
// ...
}
}
這樣,我們就可以在打出 present(.
之後,直接在自動完成列表裡選取 confirmationAlert(handler:)
來使用了。
這是因為 Xcode 的自動完成功能,會從參數的型別(UIViewController
)找 initializer(init()
、init(nibName:bundle:)
),與可回傳同型別物件的 static method(confirmationAlert(title:message:handler:)
回傳的 UIAlertController
是 UIViewController
的子類型)。如果 static method 的回傳類型不能被當作型別本身來用的話(比如說在 UIViewController
裡回傳 Int
),那它就不會出現在自動完成列表裡面。
而最後在 ViewController
裡面,就會只剩這樣的程式碼:
class ViewController: UITableViewController {
// 處理使用者動作的方法
@IBAction func deleteButtonPressed(_ sender: UIBarButtonItem) {
// 呈現確認刪除用的警告。
present(
.confirmationAlert(title: "刪除警告", message: "你確定要刪除這筆資料嗎?") {
self.deleteData()
},
animated: true, completion: nil
)
}
// 刪除資料用的方法
func deleteData() {
// ...
}
}
是不是更簡單好讀了呢?而這只需要我們多寫一個 static factory method,在 UIViewController
的 extension 裡面。
結論
其實所謂的重構,追根究底來說也就是把程式碼放到對的地方而已。用程式設計的術語來說的話,就是把它們放到對的 domain(領域),或者說 namespace(命名空間)。Static method 相對於自由函數來說,差別就在於它被放到了型別裡面,而型別本身就是一個 domain、一個 namespace。前面提到的設定用程式碼,多半是屬於被設定的物件的 domain,所以很適合放到該物件的型別裡面去。這樣子整理程式碼除了使人類讀起來更清楚之外,Xcode 也能夠依它來提供更完善的自動完成列表。搭配 Swift 的型別系統,更可以在很多場合省略掉型別的名字,使程式碼更流暢好讀。
某種方面來說,static factory method 跟 initializer 很類似,都是用來產生物件的方法,也都掛在某個型別底下。但是,static factory method 具有更強大的多型能力,也可以命名,比起 initializer 來說更為彈性。如果你希望用到 Xcode 的自動完成系統,但 initializer 又無法達成你的需求時,static factory method 可能是一個好選擇呢!