本教程是設計模式 (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 中重複出現而通用的特性,我鼓勵你採用基於設計模式的程式碼,並將它放入框架之中,這樣一來,你便可以重複使用程式碼。
再次感謝你閱讀這系列的教學。享受工作,持續學習,我們下次再會!