Swift Design Pattern 系列教程 #3:外觀模式 (Facade) 與轉接器模式 (Adapter)


本教程是設計模式 (Design Patterns) 系列的第三部分,從本系列的第一篇文章開始,我們研究了「創建」類別的工廠方法模式 (Factory Method) 與單例模式 (Singleton) 兩個範例,第二篇文章則討論了「行為」類別的觀察者模式 (Observer) 與備忘錄模式 (Memento) 兩個範例。

在本次教學中,我將會解釋屬於「結構」類別的兩個範例:外觀模式 (Facade)轉接器模式 (Adapter)。我強烈建議你先閱讀前兩篇文章,以便熟悉軟體設計模式的概念。今天,除了簡單回顧設計模式的構成之外,我並不會再次複習先前提過的所有定義,所以你最好先回顧前兩篇文章。

讓我們在接下來幾節中,簡單回顧一下設計模式的一些定義。人稱「四人幫」(Gang of Four, GoF)的 Erich Gamma、Richard Helm、 Ralph Johnson 及 John Vlissides 所著的 “Design Patterns: Elements of Reusable Object-Oriented Software”,開創、收集、並解釋了目前常見的 23 種經典軟體開發設計模式。而我們今天將專注在外觀轉接器兩種模式的討論,它們屬於 GoF 所提出的「結構」類別。

協定導向程式碼與值語義

你可以找到許多附程式碼的設計模式教學,說明關於物件導向程式設計 (OOP)、參考語義 (reference semantics)、及參考型別(類別)。我正以協定導向程式設計 (POP)、值語義 (value semantics) 與數值型別 (structs) 為基礎,努力編寫一系列設計模式教學文章。如果你已經讀過我此系列教學的前兩篇文章,希望你參考我的建議,熟讀 POP 與 OOP、以及參考語義與值語義的觀念;如果還沒讀過,我強烈建議你去釐清這些概念。本次的教學只會圍繞 POP 與值語義的部分。

設計模式

設計模式是一個非常重要的工具,讓開發者可以管理複雜的程式碼。要將其概念化的話,可以說它是一種樣版技術,而每個樣版都是量身訂做來解決相對應、重複出現、又容易識別的問題。你可以把它們視作構思程式情境的最佳實作清單,在構思的過程中你會反覆查看它們。為了讓這個定義更清晰,想想看你在使用或編寫程式碼時,有多常遵循觀察者 (observer) 設計模式就會明白了。

在觀察者模式之中,主題實例通常會使用單一的關鍵資源,並將某種改變了的狀態透過廣播方式,通知其它同樣依賴這個資源的觀察者實例,感興趣的觀察者必須告訴主題實例它們有興趣收到通知,也就是說它們必須透過訂閱才可以接收通知。在 iOS 推播通知之中,使用者必須選擇接收,才會收到通知訊息;這就是觀察者一個很好的例子。

設計模式的種類

四人幫將 23 個設計模式分為「創建 (Creational)」、「行為 (Behavioral)」及「結構 (Structural)]」三大類別。本次的教學會討論結構類別的兩種設計模式。先定義「結構」這個詞:

「某些以明確的組織模式排列的東西」及 「實體在彼此關係中元素的總和」

- https://www.merriam-webster.com/dictionary/structure

結構類別設計模式旨在幫助你明確地定義每個程式碼片段的目的,並清楚地指定該段程式碼如何與其它程式碼進行交互。這類別的設計模式大部分都可以讓你簡化程式碼的使用,通常可以透過為程式碼創建一個易於閱讀的介面來做到。由於程式碼片段不會存在於真空之中,因此為程式碼片段提供一個良好的介面,就能夠明顯並清晰地定義每個程式碼片段之間的可能關係。

外觀設計模式 (Facade Design Pattern)

「外觀」一詞被定義為「任何給予特殊建築處理的建築物表面」及「虛偽的、表面的或人造的外表或效果」

- https://www.merriam-webster.com/dictionary/facade

在大多數的情況下,我們使用外觀模式為多個可能是多而複雜的介面,創造一個簡單的介面。你可能已經有創建過一般稱為「包裝器」的東西,用來為複雜的函式庫創造一個簡單的介面,目的是為了簡化函式庫的使用。

外觀設計模式的應用範例

我在 GitHub 上的外觀模式範例 playground 檔案,展現了這個模式如何為 iOS App 可獲得的沙盒檔案系統創造一個簡單的介面。iOS 檔案系統是一個很大的子作業系統,允許你創造、讀取、刪除、移動、重新命名、並複製檔案及路徑,也允許你獲取(有時候亦可設定)關於檔案和路徑的元數據,例如列出目錄中的文件、允許你檢查文件/目錄的狀態、決定檔案是否可寫入、提供預先定義好的 Apple 傾向路徑名稱等。請注意,實際上你可以做的事情,比我所列出的還要更多。

正因為 iOS 檔案系統是一個包含許多特性及功能的大議題,它是我們利用外觀模式來簡化使用的理想對象。外觀模式讓你省去不必要的功能,避免程式碼變得混亂。相反地,外觀模式也允許你指定特定 App 所需的功能;就我的情況而言,限制功能性為我最常使用到的功能,這使得我的外觀可以重用、擴展、並維護多個 App。

我使用協定導向程式設定 (POP) 與值語義,將 iOS 檔案系統主要的功能區分,並建構成可重用和擴展的單元:協定與協定擴展。

然後我將四個協議組成一個結構,表示所有iOS應用程序都可以使用的沙盒iOS目錄(另請參見此處)。 由於您可能越來越多地遇到POP和值語義的主題,請注意,組成的術語和組合在此是同義詞。

然後,我將四個協定組成一個結構,把一個可獲取的 iOS 沙盒目錄呈現給所有 iOS App(請參考這裡)。由於之後可能經常會遇到協定導向程式設定及值語義的議題,請注意組成 (composed) 與組合 (composition) 是同義詞。

請注意,下列程式碼的 Swift 錯誤處理和常見錯誤檢查只為說明用途;這樣一來,你可以更專注理解外觀模式的使用。

外觀模式的範例程式碼

讓我們來看看我的程式碼,請確認你有跟隨 GitHub 上的 playground 程式碼。這是一個預先定義目錄,列出了 Apple 希望你完成 App 大部分工作所在的預先定義目錄:

enum AppDirectories : String {
    case Documents = "Documents"
    case Inbox = "Inbox"
    case Library = "Library"
    case Temp = "tmp"
}

藉由限制我的檔案操作程式碼到這些已知的目錄下,我可以控制複雜性,同時簡化並保持在人機介面指南的規範中。

在查看我的檔案操作核心程式碼之前,讓我們先看看基於外觀設計模式的介面,因為這是本教學的主題。我創建了 iOSAppFileSystemDirectory 結構,作為一個簡單易讀的介面,可用於 AppDirectories 列舉中每個指定目錄的公共檔案系統功能中。沒錯,我是可以包含像是創建符號式的連結、或是使用 FileHandle 類別你獨立檔案進行精細操作等內容;但是,我從不使用這些功能,而且我希望保持精簡。

我已經創建了一個由四個協定組成的外觀 (我知道你在下面程式碼只看到三個,不過其中一個是經由繼承而來) :

struct iOSAppFileSystemDirectory : AppFileManipulation, AppFileStatusChecking, AppFileSystemMetaData {
    
    let workingDirectory: AppDirectories

    init(using directory: AppDirectories) {
        self.workingDirectory = directory
    }

    func writeFile(containing text: String, withName name: String) -> Bool {
        return writeFile(containing: text, to: workingDirectory, withName: name)
    }
    
    func readFile(withName name: String) -> String {
        return readFile(at: workingDirectory, withName: name)
    }
    
    func deleteFile(withName name: String) -> Bool {
        return deleteFile(at: workingDirectory, withName: name)
    }
    
    func showAttributes(forFile named: String) -> Void {
        let fullPath = buildFullPath(forFileName: named, inDirectory: workingDirectory)
        let fileAttributes = attributes(ofFile: fullPath)
        for attribute in fileAttributes {
            print(attribute)
        }
    }
    
    func list() {
        list(directory: getURL(for: workingDirectory))
    }
    
} // end struct iOSAppFileSystemDirectory

這裡有一些用來測試 iOSAppFileSystemDirectory 結構的程式碼:

var iOSDocumentsDirectory = iOSAppFileSystemDirectory(using: .Documents)

iOSDocumentsDirectory.writeFile(containing: "New file created.", withName: "myFile3.txt")
iOSDocumentsDirectory.list()
iOSDocumentsDirectory.readFile(withName: "myFile3.txt")
iOSDocumentsDirectory.showAttributes(forFile: "myFile3.txt")
iOSDocumentsDirectory.deleteFile(withName: "myFile3.txt")

這裡是我執行上述程式碼片段後,在終端機顯示的輸出:

----------------------------
LISTING: /var/folders/5_/kd8__nv1139__dq_3nfvsmhh0000gp/T/com.apple.dt.Xcode.pg/containers/com.apple.dt.playground.stub.iOS_Simulator.Swift-Facade-Design-Pattern-1C4BD3E3-E23C-4991-A344-775D5585D1D7/Documents

File: "myFile3.txt"
File: "Shared Playground Data"

----------------------------

File created with contents: New file created.

(key: __C.FileAttributeKey(_rawValue: NSFileType), value: NSFileTypeRegular)
(key: __C.FileAttributeKey(_rawValue: NSFilePosixPermissions), value: 420)
(key: __C.FileAttributeKey(_rawValue: NSFileSystemNumber), value: 16777223)
(key: __C.FileAttributeKey(_rawValue: NSFileExtendedAttributes), value: {
    "com.apple.quarantine" = < 30303836 3b356238 36656364 373b5377 69667420 46616361 64652044 65736967 6e205061 74746572 6e3b >;
})
(key: __C.FileAttributeKey(_rawValue: NSFileReferenceCount), value: 1)
(key: __C.FileAttributeKey(_rawValue: NSFileSystemFileNumber), value: 24946094)
(key: __C.FileAttributeKey(_rawValue: NSFileGroupOwnerAccountID), value: 20)
(key: __C.FileAttributeKey(_rawValue: NSFileModificationDate), value: 2018-08-29 18:58:31 +0000)
(key: __C.FileAttributeKey(_rawValue: NSFileCreationDate), value: 2018-08-29 18:58:31 +0000)
(key: __C.FileAttributeKey(_rawValue: NSFileSize), value: 17)
(key: __C.FileAttributeKey(_rawValue: NSFileExtensionHidden), value: 0)
(key: __C.FileAttributeKey(_rawValue: NSFileOwnerAccountID), value: 502)

File deleted.

讓我們簡單討論一下剛剛用來組成 iOSAppFileSystemDirectory 的協定。我們用了 AppDirectoryNames 協定與協定擴展,來將 URL 類型的完整路徑,劃分為我在 AppDirectories 列舉中指定的 Apple 預先定義目錄:

protocol AppDirectoryNames {
    
    func documentsDirectoryURL() -> URL
    
    func inboxDirectoryURL() -> URL
    
    func libraryDirectoryURL() -> URL
    
    func tempDirectoryURL() -> URL
    
    func getURL(for directory: AppDirectories) -> URL
    
    func buildFullPath(forFileName name: String, inDirectory directory: AppDirectories) -> URL
    
} // end protocol AppDirectoryNames

extension AppDirectoryNames {
    
    func documentsDirectoryURL() -> URL {
        return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    }
    
    func inboxDirectoryURL() -> URL {
        return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent(AppDirectories.Inbox.rawValue) // "Inbox")
    }
    
    func libraryDirectoryURL() -> URL {
        return FileManager.default.urls(for: FileManager.SearchPathDirectory.libraryDirectory, in: .userDomainMask).first!
    }
    
    func tempDirectoryURL() -> URL {
        return FileManager.default.temporaryDirectory
        //urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent(AppDirectories.Temp.rawValue) //"tmp")
    }
    
    func getURL(for directory: AppDirectories) -> URL {
        switch directory {
        case .Documents:
            return documentsDirectoryURL()
        case .Inbox:
            return inboxDirectoryURL()
        case .Library:
            return libraryDirectoryURL()
        case .Temp:
            return tempDirectoryURL()
        }
    }
    
    func buildFullPath(forFileName name: String, inDirectory directory: AppDirectories) -> URL {
        return getURL(for: directory).appendingPathComponent(name)
    }
} // end extension AppDirectoryNames

AppFileStatusChecking 是我的協定及協定擴展,用來封裝我的 AppDirectories 列舉中有關存儲在目錄中的文件狀態數據。「狀態」是指確定文件是否存在、是否可寫等。

protocol AppFileStatusChecking {
    func isWritable(file at: URL) -> Bool
    
    func isReadable(file at: URL) -> Bool
    
    func exists(file at: URL) -> Bool
}

extension AppFileStatusChecking {
    func isWritable(file at: URL) -> Bool {
        if FileManager.default.isWritableFile(atPath: at.path) {
            print(at.path)
            return true
        }
        else {
            print(at.path)
            return false
        }
    }
    
    func isReadable(file at: URL) -> Bool {
        if FileManager.default.isReadableFile(atPath: at.path) {
            print(at.path)
            return true
        }
        else {
            print(at.path)
            return false
        }
    }
    
    func exists(file at: URL) -> Bool {
        if FileManager.default.fileExists(atPath: at.path) {
            return true
        }
        else {
            return false
        }
    }
} // end extension AppFileStatusChecking

AppFileSystemMetaData 是我的協定及協定擴展,用來區分列出的目錄內容,並獲取副檔名屬性,兩者都來自 AppDirectories 列舉裡的目錄:

protocol AppFileSystemMetaData {
    func list(directory at: URL) -> Bool
    
    func attributes(ofFile atFullPath: URL) -> [FileAttributeKey : Any]
}

extension AppFileSystemMetaData
{
    func list(directory at: URL) -> Bool {
        let listing = try! FileManager.default.contentsOfDirectory(atPath: at.path)
        
        if listing.count > 0 {
            print("\n----------------------------")
            print("LISTING: \(at.path)")
            print("")
            for file in listing {
                print("File: \(file.debugDescription)")
            }
            print("")
            print("----------------------------\n")
            
            return true
        }
        else {
            return false
        }
    }
    
    func attributes(ofFile atFullPath: URL) -> [FileAttributeKey : Any] {
        return try! FileManager.default.attributesOfItem(atPath: atFullPath.path)
    }
} // end extension AppFileSystemMetaData

最後,AppFileManipulation 協定與協定擴展,是用來封裝位於 AppDirectories 列舉中指定目錄中檔案的讀取、寫入、刪除、重新命名、移動、複製、及改變副檔名等資訊。

protocol AppFileManipulation : AppDirectoryNames {
    func writeFile(containing: String, to path: AppDirectories, withName name: String) -> Bool
    
    func readFile(at path: AppDirectories, withName name: String) -> String
    
    func deleteFile(at path: AppDirectories, withName name: String) -> Bool
    
    func renameFile(at path: AppDirectories, with oldName: String, to newName: String) -> Bool
    
    func moveFile(withName name: String, inDirectory: AppDirectories, toDirectory directory: AppDirectories) -> Bool
    
    func copyFile(withName name: String, inDirectory: AppDirectories, toDirectory directory: AppDirectories) -> Bool
    
    func changeFileExtension(withName name: String, inDirectory: AppDirectories, toNewExtension newExtension: String) -> Bool
}

extension AppFileManipulation {
    func writeFile(containing: String, to path: AppDirectories, withName name: String) -> Bool {
        let filePath = getURL(for: path).path + "/" + name
        let rawData: Data? = containing.data(using: .utf8)
        return FileManager.default.createFile(atPath: filePath, contents: rawData, attributes: nil)
    }
    
    func readFile(at path: AppDirectories, withName name: String) -> String {
        let filePath = getURL(for: path).path + "/" + name
        let fileContents = FileManager.default.contents(atPath: filePath)
        let fileContentsAsString = String(bytes: fileContents!, encoding: .utf8)
        print("File created with contents: \(fileContentsAsString!)\n")
        return fileContentsAsString!
    }
    
    func deleteFile(at path: AppDirectories, withName name: String) -> Bool {
        let filePath = buildFullPath(forFileName: name, inDirectory: path)
        try! FileManager.default.removeItem(at: filePath)
        print("\nFile deleted.\n")
        return true
    }
    
    func renameFile(at path: AppDirectories, with oldName: String, to newName: String) -> Bool {
        let oldPath = getURL(for: path).appendingPathComponent(oldName)
        let newPath = getURL(for: path).appendingPathComponent(newName)
        try! FileManager.default.moveItem(at: oldPath, to: newPath)
        
        // highlights the limitations of using return values
        return true
    }
    
    func moveFile(withName name: String, inDirectory: AppDirectories, toDirectory directory: AppDirectories) -> Bool {
        let originURL = buildFullPath(forFileName: name, inDirectory: inDirectory)
        let destinationURL = buildFullPath(forFileName: name, inDirectory: directory)
        // warning: constant 'success' inferred to have type '()', which may be unexpected
        // let success =
        try! FileManager.default.moveItem(at: originURL, to: destinationURL)
        return true
    }
    
    func copyFile(withName name: String, inDirectory: AppDirectories, toDirectory directory: AppDirectories) -> Bool {
        let originURL = buildFullPath(forFileName: name, inDirectory: inDirectory)
        let destinationURL = buildFullPath(forFileName: name, inDirectory: directory)
        try! FileManager.default.copyItem(at: originURL, to: destinationURL)
        return true
    }
    
    func changeFileExtension(withName name: String, inDirectory: AppDirectories, toNewExtension newExtension: String) -> Bool {
        var newFileName = NSString(string:name)
        newFileName = newFileName.deletingPathExtension as NSString
        newFileName = (newFileName.appendingPathExtension(newExtension) as NSString?)!
        let finalFileName:String =  String(newFileName)
        
        let originURL = buildFullPath(forFileName: name, inDirectory: inDirectory)
        let destinationURL = buildFullPath(forFileName: finalFileName, inDirectory: inDirectory)
        
        try! FileManager.default.moveItem(at: originURL, to: destinationURL)
        
        return true
    }
} // end extension AppFileManipulation

轉接器設計模式 (Adapter Design Pattern)

「改動 (adapt)」一詞被定義為「透過修改來使其適合(用於新用途)」

- https://www.merriam-webster.com/dictionary/adapts

「轉接器 (adapter)」一詞被定義為 「調整使裝置適用於非原本用途的附件」

- https://www.merriam-webster.com/dictionary/adapter

轉接器模式是用來讓現有的程式碼(暫稱為「A」),在不需修改原本「A」的程式碼條件下,與其他可能不完全相容於「A」的程式碼(稱為「B」)一起運作。我們可以創造某種類型的轉接器,使「A」和「B」儘管有差異也可以一起運行。請記住,原本「A」的程式碼不能被修改(不論是因為會破壞程式碼、或是因為我們沒有原始碼)。

轉接器設計模式的應用範例

我在 GitHub 的轉接器範例 playground,展示了我們如何以 iOS 檔案系統作為基礎,來討論及設計轉接器設計模式的範例。假設我們從前文部分獲得了我的 iOS 檔案系統程式碼,其中目錄和文件的所有路徑都表示為 URL 實例。考慮一種情況,我們已經獲得大量的程式碼來操作 iOS 檔案系統,但是目錄和文件的所有路徑都是用字串的實例表示,並且必須使基於 URL 的程式碼可以與以字串為基礎的程式碼一起運行。

轉接器模式的範例程式碼

讓我們看看我的程式碼。請確認你有跟著我在 GitHub 上的 playground 程式碼。為了專注在轉接器模式上,我們將會使用我的 AppDirectories 列舉和 AppDirectoryNames 協定與協定擴展刪減後的版本:

enum AppDirectories : String {
    case Documents = "Documents"
    case Temp = "tmp"
}

protocol AppDirectoryNames {
    func documentsDirectoryURL() -> URL
    
    func tempDirectoryURL() -> URL
}

extension AppDirectoryNames {
    func documentsDirectoryURL() -> URL {
        return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    }
    
    func tempDirectoryURL() -> URL {
        return FileManager.default.temporaryDirectory
    }
}

我們能夠使用的一個技巧,就是創造一個「專用」轉接器,可以給我們提供基於字串的路徑到 AppDirectories 的目錄之中,也可以給我們基於字串的路徑到儲存在 AppDirectories 之中的檔案。

// A dedicated adapter
struct iOSFile : AppDirectoryNames {
    let fileName: URL
    var fullPathInDocuments: String {
        return documentsDirectoryURL().appendingPathComponent(fileName.absoluteString).path
    }
    var fullPathInTemporary: String {
        return tempDirectoryURL().appendingPathComponent(fileName.absoluteString).path
    }
    var documentsStringPath: String {
        return documentsDirectoryURL().path
    }
    var temporaryStringPath: String {
        return tempDirectoryURL().path
    }

    init(fileName: String) {
        self.fileName = URL(string: fileName)!
    }
}

以下是用來測試 iOSFile「專用」轉接器的程式碼,請注意我的行內註解:

let iOSfile = iOSFile(fileName: "myFile.txt")
iOSfile.fullPathInDocuments
iOSfile.documentsStringPath

iOSfile.fullPathInTemporary
iOSfile.temporaryStringPath

// We STILL have access to URLs
// through protocol AppDirectoryNames.
iOSfile.documentsDirectoryURL()
iOSfile.tempDirectoryURL()

以下是 playground 檔案中的逐行註解,出現於每行程式碼同一行的最右邊,代表運行時的程式碼數值,與先前的程式碼片段互相對應。下方的註解會一對一的對應到上方的程式碼:

iOSFile
"/var/folders/5_/kd8__nv1139__dq_3nfvsmhh0000gp/T/com.apple.dt.Xcode.pg/containers/com.apple.dt.playground.stub.iOS_Simulator.Swift-Adapter-Design-Pattern-0A71F81A-9388-41F5-ACBE-52A1A61A9B99/Documents/myFile.txt"
"/var/folders/5_/kd8__nv1139__dq_3nfvsmhh0000gp/T/com.apple.dt.Xcode.pg/containers/com.apple.dt.playground.stub.iOS_Simulator.Swift-Adapter-Design-Pattern-0A71F81A-9388-41F5-ACBE-52A1A61A9B99/Documents"

"/Users/softwaretesting/Library/Developer/XCPGDevices/52E1A81A-98AF-42DE-ADCF-E69AC8FA2791/data/Containers/Data/Application/F08EFF4F-8C4F-4BB7-B220-980E16344F18/tmp/myFile.txt"
"/Users/softwaretesting/Library/Developer/XCPGDevices/52E1A81A-98AF-42DE-ADCF-E69AC8FA2791/data/Containers/Data/Application/F08EFF4F-8C4F-4BB7-B220-980E16344F18/tmp"

file:///var/folders/5_/kd8__nv1139__dq_3nfvsmhh0000gp/T/com.apple.dt.Xcode.pg/containers/com.apple.dt.playground.stub.iOS_Simulator.Swift-Adapter-Design-Pattern-0A71F81A-9388-41F5-ACBE-52A1A61A9B99/Documents/
file:///Users/softwaretesting/Library/Developer/XCPGDevices/52E1A81A-98AF-42DE-ADCF-E69AC8FA2791/data/Containers/Data/Application/F08EFF4F-8C4F-4BB7-B220-980E16344F18/tmp/

我偏好的技巧是設計一個轉接器協定,讓我基於字串路徑的程式碼能夠遵從,那麼它就可以使用 String 路徑,而非 URL 路徑。

// Protocol-oriented approach
protocol AppDirectoryAndFileStringPathNamesAdpater : AppDirectoryNames {
    
    var fileName: String { get }
    var workingDirectory: AppDirectories { get }

    func documentsDirectoryStringPath() -> String
    
    func tempDirectoryStringPath() -> String
    
    func fullPath() -> String
    
} // end protocol AppDirectoryAndFileStringPathAdpaterNames

extension AppDirectoryAndFileStringPathNamesAdpater {
   
    func documentsDirectoryStringPath() -> String {
        return documentsDirectoryURL().path
    }
    
    func tempDirectoryStringPath() -> String {
        return tempDirectoryURL().path
    }
    
    func fullPath() -> String {
        switch workingDirectory {
        case .Documents:
            return documentsDirectoryStringPath() + "/" + fileName
        case .Temp:
            return tempDirectoryStringPath() + "/" + fileName
        }
    }

} // end extension AppDirectoryAndFileStringPathNamesAdpater

struct AppDirectoryAndFileStringPathNames : AppDirectoryAndFileStringPathNamesAdpater {
    
    let fileName: String
    let workingDirectory: AppDirectories
    
    init(fileName: String, workingDirectory: AppDirectories) {
        self.fileName = fileName
        self.workingDirectory = workingDirectory
    }
    
} // end struct AppDirectoryAndFileStringPathNames

以下是ㄧ些用來測試 AppDirectoryAndFileStringPathNames 結構的程式碼,並且採用 AppDirectoryAndFileStringPathNamesAdpater 轉接器協定(繼承自 AppDirectoryNames 協定)。請注意兩行行內註解:

let appFileDocumentsDirectoryPaths = AppDirectoryAndFileStringPathNames(fileName: "myFile.txt", workingDirectory: .Documents)
appFileDocumentsDirectoryPaths.fullPath()
appFileDocumentsDirectoryPaths.documentsDirectoryStringPath()

// We STILL have access to URLs
// through protocol AppDirectoryNames.
appFileDocumentsDirectoryPaths.documentsDirectoryURL()

let appFileTemporaryDirectoryPaths = AppDirectoryAndFileStringPathNames(fileName: "tempFile.txt", workingDirectory: .Temp)
appFileTemporaryDirectoryPaths.fullPath()
appFileTemporaryDirectoryPaths.tempDirectoryStringPath()

// We STILL have access to URLs
// through protocol AppDirectoryNames.
appFileTemporaryDirectoryPaths.tempDirectoryURL()

就像剛剛一樣,以下是 playground 檔案中的逐行註解,出現在每行程式碼同一行的最右邊,代表運行時的程式碼數值,對應到先前的程式碼片段。下方的註解會一對一的對應到上方的程式碼:

AppDirectoryAndFileStringPathNames
"/var/folders/5_/kd8__nv1139__dq_3nfvsmhh0000gp/T/com.apple.dt.Xcode.pg/containers/com.apple.dt.playground.stub.iOS_Simulator.Swift-Adapter-Design-Pattern-A3DE7CC8-D60F-4448-869F-2A19556C62B2/Documents/myFile.txt"
"/var/folders/5_/kd8__nv1139__dq_3nfvsmhh0000gp/T/com.apple.dt.Xcode.pg/containers/com.apple.dt.playground.stub.iOS_Simulator.Swift-Adapter-Design-Pattern-A3DE7CC8-D60F-4448-869F-2A19556C62B2/Documents"



file:///var/folders/5_/kd8__nv1139__dq_3nfvsmhh0000gp/T/com.apple.dt.Xcode.pg/containers/com.apple.dt.playground.stub.iOS_Simulator.Swift-Adapter-Design-Pattern-A3DE7CC8-D60F-4448-869F-2A19556C62B2/Documents/

AppDirectoryAndFileStringPathNames
"/Users/softwaretesting/Library/Developer/XCPGDevices/52E1A81A-98AF-42DE-ADCF-E69AC8FA2791/data/Containers/Data/Application/CF3D4156-E773-4BC4-B117-E7BDEFA3F34C/tmp/tempFile.txt"
"/Users/softwaretesting/Library/Developer/XCPGDevices/52E1A81A-98AF-42DE-ADCF-E69AC8FA2791/data/Containers/Data/Application/CF3D4156-E773-4BC4-B117-E7BDEFA3F34C/tmp"



file:///Users/softwaretesting/Library/Developer/XCPGDevices/52E1A81A-98AF-42DE-ADCF-E69AC8FA2791/data/Containers/Data/Application/CF3D4156-E773-4BC4-B117-E7BDEFA3F34C/tmp/

結論

設計模式不但鼓勵程式碼的重用,還可以幫助你提高你程式碼的一致性、可讀性、低耦合性、可維護性、以及擴展性。當你識別出 App 中重複出現而通用的特性,我鼓勵你採用基於設計模式的程式碼,並將它放入框架之中,這樣一來,你便可以重複使用程式碼。

再次感謝你閱讀這系列的教學。享受工作,持續學習,我們下次再會!

譯者簡介:HengJay,iOS 初學者,閒暇之餘習慣透過線上 MOOC 資源學習新的技術,喜歡 Swift 平易近人的語法也喜歡狗狗,目前參與生醫領域相關應用的 App 開發,希望分享文章的同時也能持續精進自己的基礎。

LinkedIn: https://www.linkedin.com/in/hengjiewang/
Facebook: https://www.facebook.com/hengjie.wang

原文Design Patterns in Swift #3: Facade and Adapter


熱愛寫作的多產作家,亦是軟體工程師、設計師、和開發員。最近專注於 Objective-C 和 Swift 的 iOS 手機 App 開發。但對於 C#、C++、.NET、JavaScript、HTML、CSS、jQuery、SQL Server、MySQL、Oracle、Agile、Test Driven Development、Git、Continuous Integration、Responsive Web Design 等。

blog comments powered by Disqus
Shares
Share This