Using The Proxy Pattern In Swift 5
Today, we’ll take a closer look at the proxy design pattern. We’ll get the formal definition out of the way quickly and then we’ll jump into some practical examples.
What Is The Proxy Pattern?
The proxy pattern is a structural pattern that helps you limit access to another class.
In practice, the proxy is typically implemented as a wrapper class that implements the same protocol and/or exposes the same interface as the class it’s wrapping (a.k.a. a one-to-one wrapper).
This makes it easy for the proxy to add additional functionality before/after the request makes it to the wrapped object (i.e. middleware) thereby allowing you to introduce new capabilities without modifying the wrapped object’s implementation (see Liskov Substitution Principal and Open–closed principle).
Take your credit card as an example. It’s effectively a proxy for your bank account; it lets you accomplish the same things you could do with a bank account, but lets you wrap it in an extra layer of security.
Adding Additional Functionality
Ex. 1: Adding Timing & Logging
As previously mentioned, one of the most common use cases for the proxy pattern is to perform an action before and/or after the request reaches the wrapped object.
Since the proxy implements the same interface as the wrapped object, it can be used as a drop-in replacement for the original object. In this case, we can easily use the proxy pattern to enable us to log execution time.
protocol ExampleProtocol {
func performAction()
}
struct ExampleService: ExampleProtocol {
func performAction() {
// Perform a long-running complex task
}
}
struct ProfilingExampleService: ExampleProtocol {
private let exampleService = ExampleService()
func performAction() {
let startingTime = Date()
exampleService.performAction()
let endingTime = Date()
print("Time Elapsed: \(endingTime.timeIntervalSince(startingTime))")
}
}
// Usage (1)
let service = ExampleService()
// Usage (2)
let service = ProfilingExampleService()
The proxy must provide the same interface as the resource it wraps thereby allowing the call site to use either ExampleService
or ProfilingExampleService
.
This also serves as a great demonstration of the Open–closed principle and Liskov Substitution Principle as we’re able to introduce new behavior without changing the implementation of the original service.
Ex. 2: Cache
Imagine we have a service that allows us to download videos from a URL.
protocol VideoDownloadService {
func getVideo(url: String)
}class MainVideoDownloadService: VideoDownloadService {
func getVideo(url: String) {
// Download video from URL
URLSession.shared.downloadTask(with: URLRequest(url: URL(string: url)!)).resume()
}
}let videoDownloader = MainVideoDownloadService()
videoDownloader.getVideo(url: "www.youtube.com/video/1")
videoDownloader.getVideo(url: "www.youtube.com/video/2")
videoDownloader.getVideo(url: "www.youtube.com/video/3")
videoDownloader.getVideo(url: "www.youtube.com/video/2")
videoDownloader.getVideo(url: "www.youtube.com/video/3")
While this code certainly works, we would clearly benefit from having a cache of previously downloaded videos. With the use of the proxy pattern, adding this new behavior is trivial:
class SmartVideoDownloadService: VideoDownloadService {
private var cache = [String: URLSessionDownloadTask]()
private let downloader = MainVideoDownloadService() func getVideo(url: String) {
if cache[url] == nil { // Download video from URL
let downloadTask = URLSession.shared.downloadTask(with: URLRequest(url: URL(string: url)!))
cache[url] = downloadTask
downloadTask.resume()
} else {
print("We've already started downloading that video")
print("Time Remaining: \(cache[url]?.progress.estimatedTimeRemaining)")
}
}
}let videoDownloader = SmartVideoDownloadService()
videoDownloader.getVideo(url: "www.youtube.com/video/1")
videoDownloader.getVideo(url: "www.youtube.com/video/2")
videoDownloader.getVideo(url: "www.youtube.com/video/3")
videoDownloader.getVideo(url: "www.youtube.com/video/2")
videoDownloader.getVideo(url: "www.youtube.com/video/3")
Since the proxy object and the wrapped object implement the same interface, any class using the proxy will continue to think it’s interacting with the wrapped object directly while the proxy is able to orchestrate everything “behind the scenes”.
Granting Conditional Access
Since the proxy pattern allows us to “intercept” the request before it makes it to the original object, it allows us to grant conditional access to the wrapped resource.
Ex. 1: Sensitive Operations
protocol ExampleProtocol {
func performDesctructiveAction()
}struct DatabaseService: ExampleProtocol {
func performDestructiveAction() {
// Delete all tables 😈
}
}struct DatabaseServiceProxy: ExampleProtocol {
let isAdmin: Bool
private let service = DatabaseService()
func performDestructiveAction() {
guard isAdmin else { return }
service.performDestructiveAction()
}
}
Ex. 2: On-Demand Initialization
If we imagine for a moment that we didn’t have access to the lazy
keyword in Swift, we can use proxies to implement the same behavior:
protocol SuperIntenseOperation {
func performCalculation()
}
class DefaultSuperIntenseOperation: SuperIntenseOperation {
func performCalculation() {
// Starting long-runnning operation
}
}
class LazyDefaultSuperIntenseOperation: SuperIntenseOperation {
private var operation: DefaultSuperIntenseOperation?
func performCalculation() {
if operation == nil {
operation = DefaultSuperIntenseOperation()
}
operation?.performCalculation()
}
}
The call site can now use either LazyDefaultSuperIntenseOperation
or DefaultSuperIntenseOperation
interchangeably.
Ex. 3: Parental Controls
Lastly, we could use the proxy pattern and its capacity to restrict access to a resource to implement parental controls on a web browser.
protocol WebBrowser {
func goToSite(url: String)
}struct DefaultWebBrowser: WebBrowser {
func goToSite(url: String) {
// Navigate to url
}
}struct ParentalControlWebBrowser: WebBrowser {
let blockedSites = [
"www.youtube.com",
"www.twitter.com",
"www.facebook.com"
] private let browser = DefaultWebBrowser() func goToSite(url: String) {
guard !blockedSites.contains(url) else {
print("You are not allowed to visit this site. Find cooler parents.")
return
} browser.goToSite(url: url)
}
}
Adapter vs Proxy vs Decorator
Before we wrap up, the proxy pattern tends to get confused with other structural design patterns, so let’s take a moment to make sure we understand the differences:
- Adapter: provides a different interface to the wrapped object.
- Proxy: provides the same interface to the wrapped object.
- Decorator: provides an extended interface to the wrapped object. It focuses on adding responsibilities whereas a proxy focus on controlling access to an object.
Hopefully, this short article has helped demonstrate the proxy design pattern and some of the typical use cases. If you have any article requests, feel free to send me a message.
Want more articles about iOS Development & Swift? Check out my YouTube channel or follow me on Twitter. If you have an iOS interview coming up, make sure to check out my book — Ace The iOS Interview!
If you’re an indie iOS developer, make sure to check out my newsletter!
Indie Watch is an exclusive weekly hand-curated newsletter showcasing the best iOS, macOS, watchOS, and tvOS apps from developers worldwide.
Each issue features a new indie developer, so feel free to submit your iOS apps.