Design Pattern

Swift Design Pattern 系列教程 #1:工廠方法模式 (Factory Method) 與單例模式 (Singleton)

Swift Design Pattern 系列教程 #1:工廠方法模式 (Factory Method) 與單例模式 (Singleton)
Swift Design Pattern 系列教程 #1:工廠方法模式 (Factory Method) 與單例模式 (Singleton)
In: Design Pattern, Swift 程式語言

人稱「四人幫」(Gang of Four, GoF)的 Erich Gamma、Richard Helm、 Ralph Johnson 及 John Vlissides 所著的 “Design Patterns: Elements of Reusable Object-Oriented Software”,開創、收集、並解釋了目前常見的 23 種經典軟體開發設計模式 (design pattern)。本教學將會重點介紹其中兩個四人幫稱為「創建」的模式:工廠方法模式 (factory method) 以及單例模式 (singleton)

軟體開發是一種致力將現實世界情境模組化的過程,希望能夠建立工具來加強這個情境裡的使用者體驗。在財務管理方面的工具,例如銀行 App 或是購物工具如 Amazon 或 eBay 的 iOS App 等,絕對讓現在的生活比十年前來得方便。再想想我們至今走過的道路,雖然軟體 App 的功能越來越強大,對使用者亦更簡單易用;但對開發者來說,這種 App 的開發也變得更加複雜

因此,開發者建立了一系列管理複雜性的最佳實作方式,一些較熱門的例子像是物件導向程式設計 (Object-Oriented Programming)協定導向程式設計 (Protocol-Oriented Programming)數值語義 (Value Semantics)Local Reasoning,拆分大型函式為多個有良好定義介面的小型函式(像是 Swift Extension)、 語法糖 (Syntactic Sugar) 等。還有一個在眾多最佳實作最值得關注的,就是設計模式的使用。

設計模式

設計模式是一個非常重要的工具,讓開發者可以管理複雜的程式碼。要將其概念化的話,可以說它是一種樣版技術,而每個樣版都是量身訂做來解決相對應、重複出現、又容易識別的問題。你可以把它們用於構思程式情境的最佳實作清單,在構思的過程中你會反覆查看它們,像是如何從物件家族中建立物件、而不必了解物件家族的所有詳細實作細節。設計模式的重點便是它們適用於常見的場景上,因為它們是泛用的,所以可以重複使用。讓我舉一個具體的例子。

設計模式並不是特定於某些使用案例,像是迭代 (Iterating) 一個有 11 個整數 (Int) 的 Swift 陣列。舉例來說,四人幫定義迭代器 (Iterator) 模式來提供一個通用介面,以穿透集合裡的所有項目,而無需知道集合裡的複雜性(像是型別)。設計模式並不是編寫語言程式碼,而是一個解決常見軟體開發情境的一套準則或或經驗法則。

還記得我在 AppCoda 上談論過的 “Model-View-ViewModel” (MVVM) 設計模式,以及受到 Apple 與許多 iOS 開發者青睞的 “Model-View-Controller” (MVC) 設計模式嗎?

這兩個模式通常被應用在整個 App 上。MVVM 與 MVC 是架構 (Architectural) 設計模式,旨在將使用者界面 (UI) 與 App 的資料和呈現邏輯的程式碼分開,同時也將 App 的資料與核心資料處理及/或商業邏輯分開。四人幫的設計模式在性質上更為具體,旨在解決在 App 的程式庫更具體的問題。你可以在一個 App 裡使用三個、或七個、甚至十二個四人幫的設計模式。還記得我的迭代範例吧?雖然這不在四人幫的設計模式清單裡,但是代理 (Delegation) 是另一個很棒的設計模式

雖然四人幫的書對很多開發者來說就如聖經,但是亦有批評者存在的。我們將會在本篇文章的結論中討論這一點。

設計模式分類

四人幫將 23 個設計模式分為「創建 (Creational)」、「結構 (Structural)」及「行為 (Behavioral)」三大類別。此次教學會談論創建類別中的兩個模式。這個模式的目的,是要讓開發者建立物件(通常很複雜的)的過程更簡單直接、可理解及維護,並隱藏一些像實例化或物件實作的細節。

聰明的開發者的最終目標就是隱藏複雜性(封裝)。例如,物件導向 (OOP) 類別能夠提供非常複雜而強大的功能,但不需要開發者了解關於類別內部的運作。在創建模式中,開發者可能不必知道類別的關鍵屬性及方法,但如果需要的話,他可以看一眼感興趣類別的介面(也就是 Swift 的協定),然後即插即用。看完我們第一個「工廠方法」設計模型範例後,你就應該會明白了。

工廠方法設計模式 (The factory method design pattern)

如果你已經研究過四人幫的設計模式,並/或花了不少時間在 OOP 世界,那麼你大概會聽過「抽象工廠 (Abstract Factory)」、「工廠 (Factory)」或「工廠方法 (Factory Method)」模式。我將展示的範例是最接近「工廠方法」模式的。

在這個範例中,你可以建立非常有用的物件,而不需直接呼叫類別建構函式,亦不需了解任何由工廠方法實例化的類別或是類別階層結構。你會非常驚訝,原來只需要少量程式碼,就可以達到所要的功能及 UI(如適用)。我在 GitHub 上提供的工廠方法範例專案,就展示了團隊的 UI 開發者如何能夠輕鬆使用一般類別階層的物件:

design pattern Factory Method

大部分成功的 App 都有個一致的外觀,貫徹一個佈景主題,讓使用者用起來覺得愉快,而且主題亦與 App 及/或開發者有關。假設在我們假想 App 裡,所有形狀的顏色和大小都相同,以便與 App 的佈景主題保值一致 ── 這就是品牌。這些形狀能夠透過 App 來成為客製化按鈕,或是介紹流程中背景圖像的一部分。

假設設計團隊已經同意將 App 的佈景主題程式碼用於 App 背景圖像,我們將一起看看我的程式碼,從協定開始,到類別階層結構,然後是我們假想的 UI 開發者不必擔心的工廠方法。

看一下 ShapeFactory.swift 檔案。這個協定就是負責為早已存在的 ViewController 繪製形狀。因為它可能有多種用途,所以它的訪問權限是 Public:

// these values have been pre-selected by
// the graphics and design teams
let defaultHeight = 200
let defaultColor = UIColor.blue

protocol HelperViewFactoryProtocol {
    
    func configure()
    func position()
    func display()
    var height: Int { get }
    var view: UIView { get }
    var parentView: UIView { get }
    
}

記住 UIView 類別預設有一個矩形的 frame,所以它可以讓我簡單地製作 Square 基底形狀類別:

fileprivate class Square: HelperViewFactoryProtocol {
    
    let height: Int
    let parentView: UIView
    var view: UIView
    
    init(height: Int = defaultHeight, parentView: UIView) {
        
        self.height = height
        self.parentView = parentView
        view = UIView()
        
    }
    
    func configure() {
        
        let frame = CGRect(x: 0, y: 0, width: height, height: height)
        view.frame = frame
        view.backgroundColor = defaultColor
        
    }
    
    func position() {
        
        view.center = parentView.center
        
    }

    func display() {
        
        configure()
        position()
        parentView.addSubview(view)
        
    }
    
} // end class Square

請注意,我利用 OOP 的特性來重用程式碼,讓形狀階層結構簡化而可維護。類別 CircleRectangleSquare 的特化(請記住由一個完美的方形畫出一個圓形是非常容易的):

fileprivate class Circle : Square {
    
    override func configure() {
        
        super.configure()
        
        view.layer.cornerRadius = view.frame.width / 2
        view.layer.masksToBounds = true
        
    }
    
} // end class Circle

fileprivate class Rectangle : Square {
    
    override func configure() {
        
        let frame = CGRect(x: 0, y: 0, width: height + height/2, height: height)
        view.frame = frame
        view.backgroundColor = UIColor.blue
        
    }
    
} // end class Rectangle

我用了 fileprivate 來加強工廠方法模式的一個目的:隱藏複雜性。你應該也看到,我們無須改變下面的工廠方法,也可以輕易調整或是被延伸形狀類別階層架構。以下是工廠方法的程式碼,可以讓物件建立變得抽象及簡單:

enum Shapes {
    
    case square
    case circle
    case rectangle
    
}

class ShapeFactory {
    
    let parentView: UIView
    
    init(parentView: UIView) {
        
        self.parentView = parentView
        
    }
    
    func create(as shape: Shapes) -> HelperViewFactoryProtocol {
        
        switch shape {
            
        case .square:
            
            let square = Square(parentView: parentView)
            return square
            
        case .circle:
            
            let circle = Circle(parentView: parentView)
            return circle
            
        case .rectangle:
            
            let rectangle = Rectangle(parentView: parentView)
            return rectangle
            
        }
        
    } // end func display
    
} // end class ShapeFactory

// Public factory method to display shapes.
func createShape(_ shape: Shapes, on view: UIView) {
    
    let shapeFactory = ShapeFactory(parentView: view)
    shapeFactory.create(as: shape).display()
    
}

// Alternative public factory method to display shapes.
// Technically, the factory method should return one of
// a number of related classes.
func getShape(_ shape: Shapes, on view: UIView) -> HelperViewFactoryProtocol {
    
    let shapeFactory = ShapeFactory(parentView: view)
    return shapeFactory.create(as: shape)
    
}

請注意,我已經寫了一個工廠類別和兩個工廠方法,來讓你消化一下這個設計模式。技術上來說,工廠方法應該回傳眾多相關類別中的其中一個,而這些類別都具有公開的基底類別及/或公開的協定。因為這裡的目的是要在視圖中繪製一個形狀,所以我較喜歡 createShape(_:view:) 方法。有時提供替代方案是個好主意,可以用來實驗及探索新的可能性。

最後,我讓你看一下如何用兩個工廠方法來繪製形狀。UI 開發者不必知道任何有關形狀類別如何被編碼,更不用了解形狀類別是如何被初始化。在 ViewController.swift 檔案裡的程式碼是相當簡易好讀的:

import UIKit

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func drawCircle(_ sender: Any) {
        
        // just draw the shape
        createShape(.circle, on: view)
        
    }
    
    @IBAction func drawSquare(_ sender: Any) {

        // just draw the shape
        createShape(.square, on: view)
        
    }
    
    @IBAction func drawRectangle(_ sender: Any) {

        // actually get an object from the factory
        // and use it to draw the shape
        let rectangle = getShape(.rectangle, on: view)
        rectangle.display()
        
    }
    
} // end class ViewController

單例設計模式 (The singleton design pattern)

大部分的 iOS 開發者都對單例模式十分熟悉。這些你如果想要發送通知、在 Safari 開啟 URL、或是操作 iOS 檔案時,就必須使用 UNUserNotificationCenter.current()UIApplication.sharedFileManager.default 這些單例。單例的好處在於可以保護共享資源、提供存取予只有一個物件實例的系統、支援執行 App 範圍內協作型別的物件、並能夠提供一個內建於 iOS 單例的增值 Wrapper,這一點我們將會加以詳述。

為了實作一個單例,我們要確認這個類別:

  • 宣告並初始化一個自己靜態常數屬性,並命名為 shared 來傳達類別的實例是一個單例(預設為 Public);

  • 宣告一個我們想要控制/保護、同時也由 shared 分享的 private 屬性;

  • 宣告一個 private 初始器,讓我們的單例可以自我初始化,而在這個 init 裡,我們初始化了想要控制並共享的資源。

藉由建立一個類別的 private 初始器、並定義 shared 靜態常數,我們確保了只會有一個類別的實例,而且這個類別只能自我初始化一次,同時類別的 shared 實例可以在整個 App 中被存取。就這樣,我們成功建立了一個單例

我在 GitHub 上的單例範例專案,展示了開發團隊如何安全並持續地儲存使用者偏好,而錯誤亦較少。以下是我的範例 App,記住了使用者密碼為未加密文字或加密字樣的偏好。雖然這不是個最好的例子,但我需要一個範例來展示程式碼的運作。這段程式碼只為教學目的,我建議你絕對不要讓密碼暴露出來。下面是使用者如何能夠設定自己的密碼偏好,然後將偏好儲存於 UserDefaults

Show_Pwd

When the user closes and eventually comes back to the app, note that her/his password preference is remembered:

當使用者關閉 App 然後再次回到 App 時,你會看到使用者的密碼偏好被記住了:

讓我們看看 PreferencesSingleton.swift 檔案裡的一段程式碼節錄,裡面有些註解,看完你就會清楚了解我在說甚麼了:

class UserPreferences {

    // Create a static, constant instance of
    // the enclosing class (itself) and initialize.
    static let shared = UserPreferences()
    
    // This is the private, shared resource we're protecting.
    private let userPreferences: UserDefaults
    
    // A private initializer can only be called by
    // this class itself.
    private init() {
        
        // Get the iOS shared singleton. We're
        // wrapping it here.
        userPreferences = UserDefaults.standard
        
    }

} // end class UserPreferences

就我對 Swift 的了解,我們不需要擔心 App 啟動時靜態屬性及全域變數的初始器會延遲運行。

你可能會問「為什麼我們要為另一個單例 UserDefaults 建立一個單例 Wrapper?」。首先,我這裡主要的目的是向你展示在 Swift 中建立及使用單例的最佳實作範例,然後使用者偏好應該是只有單一進入點的資源類型。所以 UserDefaults 是一個非常明顯的教學例子。但,想一想你在 App 的程式庫看到UserDefaults 被使用了(濫用)多少次。

我看過 App 裡的 UserDefaults(或「以前」的 NSUserDefaults)沒來由的遍佈整個專案程式碼,在使用者偏好裡每個單一對應的鍵都是人手拼出來的,後來我在程式碼中發現了一個 Bug,就是我把 “switch” 拼成 “swithc”,然後我一直對這個錯誤複製貼上,結果在發現問題前已經留下了很多 “swithc” 實例。如果其他團隊成員啟動 App 或是繼續使用 “switch” 為儲存相關資料的鍵,會發生甚麼事?App 原本應該只有一個狀態被保存起來,但這樣的情況最終可能會有兩個或更多的狀態被保存。UserDefaults 使用字串為我們希望維護的部分 App 狀態的數值關鍵,這完全沒有問題,因為最好使用有意義、容易識別、且容易記住的單詞,來描述數值。但是字串也不是沒有風險的。

你們大部分可能讀過關於被稱為「泛字串型別 (Stringly-Typed)」的程式碼,就像我剛才關於 “swithc” 與 “switch” 的討論。雖然字串是非常有描述性的,但是使用字串作為整個程式庫的唯一識別,可能只因為拼寫錯誤,而導致細微但災難性的錯誤。Swift 編譯器無法讓我們避免產生泛字串型別錯誤。

使用 enum 形式的字串常數,就可以解決泛字串型別錯誤。它不但可以標準化我們的字串使用,也可以將字串組織成不同類別。再看一次 PreferencesSingleton.swift

...
class UserPreferences {
    
    enum Preferences {
        
        enum UserCredentials: String {
            case passwordVisibile
            case password
            case username
        }
        
        enum AppState: String {
            case appFirstRun
            case dateLastRun
            case currentVersion
        }

    } // end enum Preferences
...

雖然我開始徘徊於單例設計模式的的定義裡,但我還是想簡單展示並解釋在大部分 App 的 UserDefaults 使用單例 Wrapper 的原因。有很多增值功能可以讓 UserDefaults 單例 Wrapper 變得更方便,同時增加程式碼的可靠性,例如是取得及設定偏好時立即提供錯誤判斷。而另一個我想添加的功能,就是為常用使用者偏好提供方便的方法,像是如何處理密碼。你在閱讀下面的程式碼時,就會看到剛剛提過的部份。以下是我PreferencesSingleton.swift 檔案裡的程式碼:

import Foundation

class UserPreferences {
    
    enum Preferences {
        
        enum UserCredentials: String {
            case passwordVisibile
            case password
            case username
        }
        
        enum AppState: String {
            case appFirstRun
            case dateLastRun
            case currentVersion
        }

    } // end enum Preferences
    
    // Create a static, constant instance of
    // the enclosing class (itself) and initialize.
    static let shared = UserPreferences()
    
    // This is the private, shared resource we're protecting.
    private let userPreferences: UserDefaults
    
    // A private initializer can only be called by
    // this class itself.
    private init() {
        
        // Get the iOS shared singleton. We're
        // wrapping it here.
        userPreferences = UserDefaults.standard

    }
    
    func setBooleanForKey(_ boolean:Bool, key:String) {
        
        if key != "" {
            userPreferences.set(boolean, forKey: key)
        }
        
    }
    
    func getBooleanForKey(_ key:String) -> Bool {
        
        if let isBooleanValue = userPreferences.value(forKey: key) as! Bool? {
            print("Key \(key) is \(isBooleanValue)")
            return true
        }
        else {
            print("Key \(key) is false")
            return false
        }
        
    }
    
    func isPasswordVisible() -> Bool {
        
        let isVisible = userPreferences.bool(forKey: Preferences.UserCredentials.passwordVisibile.rawValue)
        
        if isVisible {
            return true
        }
        else {
            return false
        }
        
    }

    func setPasswordVisibity(_ visible: Bool) {
        
        userPreferences.set(visible, forKey: Preferences.UserCredentials.passwordVisibile.rawValue)
        
    }

} // end class UserPreferences

看看我的 ViewController.swift 檔案,就會發現存取及使用架構良好的單例是多麼簡單:

import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var passwordVisibleSwitch: UISwitch!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        if UserPreferences.shared.isPasswordVisible() {
            passwordVisibleSwitch.isOn = true
            passwordTextField.isSecureTextEntry = false
        }
        else {
            passwordVisibleSwitch.isOn = false
            passwordTextField.isSecureTextEntry = true
        }
        
    } // end func viewDidLoad

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    @IBAction func passwordVisibleSwitched(_ sender: Any) {
        
        let pwdSwitch:UISwitch = sender as! UISwitch
        
        if pwdSwitch.isOn {
            passwordTextField.isSecureTextEntry = false
            UserPreferences.shared.setPasswordVisibity(true)
        }
        else {
            passwordTextField.isSecureTextEntry = true
            UserPreferences.shared.setPasswordVisibity(false)
        }
        
    } // end func passwordVisibleSwitched
    
} // end class ViewController

總結

一些評論說使用設計模式就證明了程式語言的不足之處,而且經常在程式碼看到模式出現不太好;我並不同意這看法。期待程式語言擁有所有功能是不切實際的,而且可能會導致本來已經很巨大的程式語言如 C++ 變得更巨大、更複雜,以致難以學習、使用和維護。了解並解決經常發生的問題是一個積極的特質,值得積極鼓勵。設計模式是人類從歷史中學到的成功範例。針對常見問題來寫出摘要並提出標準解決方法,讓這些解決方法可以被移植及分散出去。

像 Swift 這般的簡潔程式語言與最佳實踐範例(像是設計模式)結合,是一個理想而有趣的媒介。一致的程式碼通常都是可讀且可維護的。還要記住,隨著數百萬開發者不斷討論及分享想法,設計模式仍不斷發展。藉由網際網路連結在一起,這種開發者的討論就形成了不斷自我調整的集體智慧。

譯者簡介:楊敦凱-目前於科技公司擔任 iOS Developer,工作之餘開發自有 iOS App同時關注網路上有趣的新玩意、話題及科技資訊。平時的興趣則是與自身專業無關的歷史、地理、棒球。來信請寄到:[email protected]

原文Design Patterns in Swift #1: Factory Method and Singleton

作者
Andrew Jaffee
熱愛寫作的多產作家,亦是軟體工程師、設計師、和開發員。最近專注於 Swift 的 iOS 手機 App 開發。但對於 C#、C++、.NET、JavaScript、HTML、CSS、jQuery、SQL Server、MySQL、Agile、Test Driven Development、Git、Continuous Integration、Responsive Web Design 等。
評論
更多來自 AppCoda 中文版
策略模式 (Strategy Pattern)簡介 讓程式碼拓展起來更容易
Design Pattern

策略模式 (Strategy Pattern)簡介 讓程式碼拓展起來更容易

本篇原文(標題:Understanding the Strategy Pattern )刊登於作者 Medium,由 Jimmy M Andersson 所著,並授權翻譯及轉載。 我們在編寫類別時,有時會用上大量看上去很相似的方法,但礙於它們在計算方式上存在關鍵的差異,讓我們無法編寫一個通用函數,而刪減其他的函數。今天,
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。