善用 Static Factory Method 重構程式碼 讓它更流暢好讀!


在使用一個物件之前,我們經常會需要對其進行設定。比如說,使用一個 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.viewUIView 物件,makeView()makeLoadingViewController 都會同時出現。

free-function-factory-method

所以,雖然自由函數工廠方法是很好的重構與解耦工具,但使用上總是有那麼一點的不方便。還好 Swift 提供了另一種工具,既具備自由函數的解耦性,又能充分運用 Xcode 的自動完成系統,那就是 ⋯⋯

Static Factory Method!

在 Swift 裡,只要在某個方法前面加上 static 關鍵字,就會使它成為是屬於那個型別本身的方法。如果我們把型別當成是製造物件的藍圖的話,那 static method 就等於是藍圖本身的方法。而由於藍圖本身最大的用途就是拿來製造物件,所以它的方法也就常常都是工廠方法,用來補全 initializer 所不能做到的事。

甚麼是 initializer 做不到的呢?

  1. 命名:Initializer 只能對參數命名,沒辦法對方法本身命名。所以,同一個型別裡沒辦法有兩個不同的沒有參數的 initializer。
  2. 多型: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:

Static-Factory-Method-1

這樣就可以過濾掉所有不相關的函數與方法了。

實戰重構 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:) 來使用了。

Static-Factory-Method-2

這是因為 Xcode 的自動完成功能,會從參數的型別(UIViewController)找 initializer(init()init(nibName:bundle:)),與可回傳同型別物件的 static method(confirmationAlert(title:message:handler:) 回傳的 UIAlertControllerUIViewController 的子類型)。如果 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 可能是一個好選擇呢!


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

blog comments powered by Disqus
Shares
Share This