This tutorial is the third installment in our series on design patterns. I started
this series with a tutorial examining two examples of patterns in the “creational” category: factory method and singleton. I then discussed two examples of patterns in the “behavioral” category: observer and memento.
In this tutorial, I’ll explain two examples of patterns in the “structural” category: facade and adapter. I urge you to review my first two posts mentioned above so you can familiarize yourself with the concept of software design patterns. Beyond a brief reminder today of what constitutes a design pattern, I’m not going to regurgitate all the definitions again. All the information you need to get up to speed is in my first two tutorials, here and here.
Let’s briefly review some general definitions of design patterns here and in the next few sections. There are 23 classic software development design patterns probably first identified, collected, and explained all in one place by the “Gang of Four” (“GoF”), Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides in their seminal book, “Design Patterns: Elements of Reusable Object-Oriented Software.”. Remember that today we’ll focus on two of these patterns, facade and adapter, which fall into what the GoF calls the “structural” category.
Protocol-oriented code with value semantics
You’ll still find that many tutorials about design patterns contain sample code based on object-oriented programming (OOP) principles, reference semantics, and reference types (classes). I am endeavoring to create a series of tutorials on design patterns that are mostly based in protocol-oriented programming principles (POP), value semantics, and value types (structs). If you’ve reviewed the first two articles in this series, I hope you followed my advice and familiarized yourselves with POP versus OOP and reference semantics versus value semantics. If not, I strongly encourage you to get up to speed on these topics. This tutorial is solely based in POP and value semantics.
Design Patterns
Design patterns are an extremely important tool with which developers can manage complexity. It’s best to conceptualize them as generally templated techniques, each tailored to solving a corresponding, recurring, and readily identifiable problem. Look at them as a list of best practices you would use for coding scenarios that you see over and over again. To make this definition tangible, think of how many times you’ve either used code or written code that conforms to the observer design pattern.
In observer, the subject instance, usually a single critical resource, broadcasts notifications about a change in its state to many observer instances that depend on that resource. Interested observers must tell the subject instance that they’re interested in receiving notifications, in other words, they must subscribe to get notifications. Encoding iOS push notifications, where users must opt into receiving messages, is a great example of observer.
Design pattern categories
The GoF organized their 23 design patterns into three categories, “creational,” “behavioral,” and “structural.” This tutorial discusses two patterns in the structural category. Consider the word “structure,”
“something arranged in a definite pattern of organization” and “the aggregate of elements of an entity in their relationships to each other.”
– https://www.merriam-webster.com/dictionary/structure
Structural design patterns are meant to help you to clearly define the purpose of a segment of code and clearly specify how other code interacts with that segment. Most patterns in this category enable you to simplify the use of your code. We can usually simplify use of code by creating an easily readable interface to that code. Since pieces/segments of code don’t exist in a vacuum, providing a good interface to a code segment should obviously and cleanly define the possible relationships that can be built between that segment and other segments.
The facade design pattern
The word “facade” is defined as “any face of a building given special architectural treatment” and “a false, superficial, or artificial appearance or effect.”
– https://www.merriam-webster.com/dictionary/facade
In most cases, we use the facade pattern to create one simple interface to a group of other, possibly many, and usually complex, interfaces. You have probably already created what are commonly called “wrappers” where you built a simple interface to a complex codebase with the purpose of simplifying the use of that codebase.
Use case for facade design pattern app
My facade example playground, available on GitHub, showcases how this pattern can create one simple interface to the sandboxed file system available to each iOS app. The iOS file system is a huge OS subsystem, allowing you to create, read, delete, move, rename, and copy files and directories; allowing you to get (and sometimes set) meta data about files and directories, e.g., list the files in a directory; allowing you to check the status of files/directories, e.g., determine if a file is writable; and, providing you with the names of predefined directories in which Apple prefers that you work. Note that you can do much more than what I just listed.
Since the iOS file system is such a grand topic with many different features and functions, it is an ideal candidate for using the facade design pattern to simplify its usage. A facade interface allows you to leave out functionality you don’t need and that may clutter your code. Conversely, a facade interface allows you to specify only the functionality you need for a particular app, or in my case, to limit functionality to what I have found to be only the features I have used over time, and thus make my facade reusable, extensible, and maintainable for many of my apps.
I used protocol-oriented programming and value semantics for dividing and conquering major features of the iOS file system into reusable and extensible units: protocols and protocol extensions.
I then composed four protocols into one struct that represents a sandboxed iOS directory available to all iOS apps (see also here). Since you’re likely to run across the topics of POP and value semantics more and more, note that the terms composed and composition are synonymous herein.
Note that I left Swift error handling and more generic error checking out of the code shown below solely for didactic purposes, i.e., so you can more easily concentrate only on understanding use of the facade pattern.
Sample code for the facade design pattern
Let’s walk through my code. Make sure you follow along with my code in the playground hosted on GitHub. Here’s a list of predefined directories in which Apple prefers you do much of your iOS app’s work:
enum AppDirectories : String { case Documents = "Documents" case Inbox = "Inbox" case Library = "Library" case Temp = "tmp" }
By constraining my file manipulation code to these known directories, I control complexity, simplify, and stay within the bounds of the Human Interface Guidelines.
Before looking at my core code for file manipulation, let’s first look at my facade design pattern-based interface as that’s the topic of this tutorial. I created the iOSAppFileSystemDirectory
struct as a simple and readable interface to common file system features available for each of the directories specified in my AppDirectories
enum. Yes, I could get involved with things like the creation of symbolic links or the use of fine grained manipulation of individual files using the FileHandle
class, but I almost never use these features, and most importantly, I’m deliberately keeping things simple.
I’ve created a facade composed of four protocols (I know you see three immediately below, but one of the protocols inherits from another):
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
Here’s some code that tests my iOSAppFileSystemDirectory
struct:
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")
Here’s the output to console from executing the previous code snippet in my playground:
---------------------------- 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.
Let’s briefly discuss the protocols that were used to compose iOSAppFileSystemDirectory
. Here we have the AppDirectoryNames
protocol and protocol extension which compartmentalize the retrieval of full paths of type URL
to the Apple-predefined directories as specified in my AppDirectories
enum:
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
is my protocol and protocol extension that encapsulate getting state data about files stored in directories as specified in my AppDirectories
enum. By “state,” I mean determining if a file exists, if it’s writable, etc.
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
is my protocol and protocol extension that compartmentalize listing directory contents and getting extended file attributes, both from directories as specified in my AppDirectories
enum:
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
Finally, the AppFileManipulation
protocol and protocol extension encapsulate the reading, writing, deleting, renaming, moving, copying, and changing the file extension of files located in directories as specified in my AppDirectories
enum:
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
The adapter design pattern
The word “adapt” is defined as “to make fit (as for a new use) often by modification.”
– https://www.merriam-webster.com/dictionary/adapts
The word “adapter” is defined as “an attachment for adapting apparatus for uses not originally intended.”
– https://www.merriam-webster.com/dictionary/adapter
The adapter pattern is used so that an existing codebase (call it “A”) can work, without modifying the original “A” code, with other code (call it “B”) that may not be completely compatible with the existing, original codebase “A.” We can create some kind of adapter that allows “A” and “B” to work together despite their differences. Remember that the existing, original codebase “A” cannot be modified (either because it would break the code or because we don’t have the source).
Use case for adapter design pattern app
My adapter example playground, available on GitHub, showcases how we’ll use the iOS file system as a substrate on which to discuss and design an example of the adapter pattern. Let’s say we’ve got my iOS file system code from the preceding sections up above where all paths to directories and files are expressed as URL
instances. Consider a scenario in which we’ve been given a huge chunk of code that also manipulates the iOS file system, but where all paths to directories and files are expressed as String path instances, and the URL-based code must be made to work with the String path-based code.
Sample code for the adapter design pattern
Let’s walk through my code. Make sure you follow along with my code in the playground hosted on GitHub. In order to concentrate solely on the adapter pattern, we’ll use slimmed-down versions of my AppDirectories
enum and AppDirectoryNames
protocol and protocol extension:
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 } }
One technique we could use is to create a “dedicated” adapter, one which gives us string-based paths to directories as specified in AppDirectories
and also gives us string-based paths to files stored in directories as specified in 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)! } }
Here’s some code that tests the iOSFile
“dedicated” adapter — and note my one inline comment:
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()
Here are my playground’s line-by-line annotations which appear to the right of each line of code, on the same line, representing runtime code values, corresponding to the previous code snippet — the annotations below correspond one-to-one to the lines of code above:
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/
The technique I prefer is to design an adapter protocol that my string path-based code could adopt so that it could use String
paths instead of URL
paths.
// 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
Here’s some code that tests my AppDirectoryAndFileStringPathNames
struct, which adopts the AppDirectoryAndFileStringPathNamesAdpater
adapter protocol (which is a descendent of the AppDirectoryNames
protocol) — and note my two inline comments:
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()
Here are my playground’s line-by-line annotations which appear to the right of each line of code, on the same line, representing runtime code values, corresponding to the previous code snippet — the annotations below correspond one-to-one to the lines of code above:
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/
Conclusion
Not only do design patterns encourage code reuse, but can help you make your code consistent, readable, loosely coupled, and thus maintainable and extensible. When you identify recurring and generalized features in your apps, I encourage you to take your design pattern-based code and put it into frameworks so you write it once and use it many more times.
Thanks for joining me again here on AppCoda. Enjoy your work, keep on learning, and I’ll see you soon!