Introduction
The proxy object serves as an interface to the original object and can be applied to various resources, such as a network connection, a large file, or other resources that are expensive or difficult to duplicate. Simply put, the proxy object is a specialized wrapper for the original object that can control access to it and perform additional actions either before or after forwarding a request to the original object.
In other words, the Proxy pattern can significantly simplify the design of an application. If an object requires additional responsibilities, it must be wrapped in a special wrapper that encapsulates this extra functionality. This pattern is very similar to the Decorator design pattern, but there are some key differences that will be explained later.
Definition
The Proxy design pattern is a structural design pattern that allows you to provide a placeholder object for the original object. The proxy object can control access to the original object and may perform actions either before or after forwarding a request to the original object.
What kind of problems can the Proxy design pattern solve?
- We want to control access to the original object and perform actions before or after forwarding a request to it.
What solutions does the Proxy design pattern describe?
- Define a separate proxy object that implements the same interface as the original object.
- Implement additional functionality to control access to the original object.
This makes it possible to work with the proxy object when the original interface is applied. For example, the proxy object may contain the behavior for checking the user's rights to perform certain actions.
SRP & OCP
The Proxy design pattern follows the SRP and OCP principles. The Proxy object usually has only one responsibility: controlling access to the original object or adding additional behavior without modifying the original class.
This is achieved by implementing the same interface as the original object, allowing the Proxy object to act as a substitute. Proxies are commonly used for purposes such as lazy initialization, access control, logging, caching, or remote object representation.
When applied to the Proxy design pattern, the OCP ensures that the behavior of a system can be extended by introducing new proxies without altering the behavior of the existing code of the real object or other components.
Similarity with Other Design Patterns
Design Pattern | Description |
---|---|
Adapter | Converts one interface into another, enabling these interfaces to collaborate. |
Facade | Provides a simplified interface for the client. |
Decorator | Dynamically adds responsibilities to the wrapped object without altering its original structure. |
UML Diagram
The proxy object implements the Subject
interface and can act as a placeholder for Subject
objects. It maintains a reference to the original object and can forward requests to it.
Examples
Sensitive Operations
Since the Proxy design pattern can control access to the original object, it enables granting conditional access to the wrapped resource.
protocol IExampleService {
func operation()
}
final class ExampleService: IExampleService {
func operation() {
// Perform the opertation
}
}
final class ExampleServiceProxy: IExampleService {
// MARK: Properties
private let isAdmin: Bool
private let service: ExampleService
// MARK: Initialization
init(service: IExampleService, isAdmin: Bool) {
self.service = service
self.isAdmin = isAdmin
}
// MARK: IExampleService
func operation() {
guard isAdmin else { return }
service.operation()
}
}
let service = ExampleService()
let serviceProxy = ExampleServiceProxy(service: service, isAdmin: true)
Logging
Using the Proxy design pattern, the logging mechanism can be added easily.
protocol ExampleProtocol {
func operation()
}
final class ExampleService: ExampleProtocol {
func operation() {
// Implement the operation.
}
}
final class ProfilingExampleService: ExampleProtocol {
// MARK: Properties
private let service: ExampleProtocol
// MARK: Initialization
init(service: ExampleProtocol) {
self.service = service
}
// MARK: ExampleProtocol
func operation {
let startingTime = Date()
service.operation()
let endingTime = Date()
print("operation: startingTime: \(startingTime), endingTime: \(endingTime)")
}
}
let exampleService = ExampleService()
let profilingExampleService = ProfilingExampleService(service: exampleService)
Cache
Imagine that you need to download something from the internet and want to cache the downloaded data. This can be achieved using the Proxy design pattern. Since the original object and the proxy object conform to the same interface, these objects are interchangeable. This allows you to implement a new proxy object with additional caching functionality.
protocol IImageDownloadService {
func download(url: URL) throws async -> UIImage
}
final class ImageDownloadService: IImageDownloadService {
func download(url: URL) throws async -> UIImage {
// Implementation of downloading an image.
}
}
final class CacheImageDownloadService: IImageDownloadService {
// MARK: Types
private enum Error: Swift.Error {
case downloadAlreadyStarted
}
// MARK: Properties
private let service: IImageDownloadService
private var isLoading = Set<String>()
// MARK: Initialization
init(service: IImageDownloadService) {
self.service = service
}
// MARK: IImageDownloadService
func download(url: URL) throws async -> UIImage {
guard !isLoading.contain(url) else {
throw Error.downloadAlreadyStarted
}
isLoading.insert(url)
do {
return try await service.download(url)
} catch {
throw error
}
}
}
let imageDownloadService = ImageDownloadService()
let cacheImageDownloadService = CacheImageDownloadService(service: imageDownloadService)
On Demand Initialization
Behavior similar to the lazy
keyword in Swift can be implemented using the Proxy design pattern.
protocol IExampleOperation {
func operation()
}
final class DefaultOperation: IExampleOperation {
func operation() {
// Implement the operation.
}
}
final class LazyDefaultOperation: IExampleOperation {
// MARK: Properties
private var operation: DefaultOperation?
// MARK: IExampleOperation
func operation() {
if operation == nil {
operation = DefaultOperation()
}
operation?.operation()
}
}
Summary
In this article, we explored the Proxy design pattern, a structural pattern that provides a placeholder for another object to control access to it. We discussed various use cases, including access control, caching, and lazy initialization, demonstrating how proxies act as intermediaries to add functionality without altering the original object's code.
Top comments (0)