The Proxy design pattern is a structural pattern that acts as a stand-in or “placeholder” for another object, helping control access to it. By applying this pattern, you add an extra layer that manages an object’s behavior, all without altering the original object. In this article, we’ll explore the basics of the Proxy pattern, dive into real-world examples where it proves useful, and guide you through a Kotlin implementation with step-by-step explanations to make everything clear and approachable.
Proxy Design Pattern
In programming, objects sometimes need additional layers of control—whether it’s for performance optimizations, access restrictions, or simplifying complex operations. The Proxy pattern achieves this by creating a “proxy” class that represents another class. This proxy class controls access to the original class and can be used to introduce additional logic before or after the actual operations.
It’s particularly useful in situations where object creation is resource-intensive, or you want finer control over how and when the object interacts with other parts of the system. In Android, proxies are commonly used for lazy loading, network requests, and logging.
When to Use Proxy Pattern
The Proxy pattern is beneficial when:
- You want to control access to an object.
- You need to defer object initialization (lazy initialization).
- You want to add functionalities, like caching or logging, without modifying the actual object.
Structure of Proxy Pattern
The Proxy Pattern involves three main components:
- Subject Interface – Defines the common interface that both the
RealSubject
andProxy
implement. - RealSubject – The actual object being represented or accessed indirectly.
- Proxy – Controls access to the
RealSubject
.
Types of Proxies
Different types of proxies serve distinct purposes:
- Remote Proxy: Manages resources in remote systems.
- Virtual Proxy: Manages resource-heavy objects and instantiates them only when needed.
- Protection Proxy: Controls access based on permissions.
- Cache Proxy: Caches responses to improve performance.
Real-World Use Cases
The Proxy pattern is widely used in scenarios such as:
- Virtual Proxies: Delaying object initialization (e.g., for memory-heavy objects).
- Protection Proxies: Adding security layers to resources (e.g., restricting access).
- Remote Proxies: Representing objects that are in different locations (e.g., APIs).
Let’s consider a video streaming scenario, similar to popular platforms like Disney+ Hotstar, Netflix, or Amazon Prime. Here, a proxy can be used to control access to video data based on the user’s subscription type. For instance, the proxy could restrict access to premium content for free-tier users, ensuring that only eligible users can stream certain videos. This adds a layer of control, enhancing security and user experience while keeping the main video service logic clean and focused.
Here’s the simple proxy implementation,
First we define the interface
interface VideoService {
fun streamVideo(videoId: String): String
}
Create the Real Object
The RealVideoService
class represents the actual video streaming service. It implements the VideoService
interface and streams video.
class RealVideoService : VideoService {
override fun streamVideo(videoId: String): String {
// Simulate streaming a large video file
return "Streaming video content for video ID: $videoId"
}
}
Create the Proxy Class
The VideoServiceProxy
class controls access to the RealVideoService
. We’ll implement it so only premium users can access certain videos, adding a layer of security.
class VideoServiceProxy(private val isPremiumUser: Boolean) : VideoService {
// Real service reference
private val realVideoService = RealVideoService()
override fun streamVideo(videoId: String): String {
return if (isPremiumUser) {
// Delegate call to real object if the user is premium
realVideoService.streamVideo(videoId)
} else {
// Restrict access for non-premium users
"Upgrade to premium to stream this video."
}
}
}
Testing the Proxy
Now, let’s simulate clients trying to stream a video through the proxy.
fun main() {
val premiumUserProxy = VideoServiceProxy(isPremiumUser = true)
val regularUserProxy = VideoServiceProxy(isPremiumUser = false)
println("Premium User Request:")
println(premiumUserProxy.streamVideo("premium_video_123"))
println("Regular User Request:")
println(regularUserProxy.streamVideo("premium_video_123"))
}
Output
Premium User Request:
Streaming video content for video ID: premium_video_123
Regular User Request:
Upgrade to premium to stream this video.
Here,
- Interface (VideoService): The
VideoService
interface defines the contract for streaming video. - Real Object (RealVideoService): The
RealVideoService
implements theVideoService
interface, providing the actual video streaming functionality. - Proxy Class (VideoServiceProxy): The
VideoServiceProxy
class implements theVideoService
interface and controls access toRealVideoService
. It checks whether the user is premium and either allows streaming or restricts it. - Client: The client interacts with
VideoServiceProxy
, not withRealVideoService
. The proxy makes the access control transparent for the client.
Benefits and Limitations
Benefits
- Controlled Access: Allows us to restrict access based on custom logic.
- Lazy Initialization: We can load resources only when required.
- Security: Additional security checks can be implemented.
Limitations
- Complexity: It can add unnecessary complexity if access control is not required.
- Performance: May slightly impact performance because of the extra layer.
Conclusion
The Proxy design pattern is a powerful tool for managing access, adding control, and optimizing performance in your applications. By introducing a proxy, you can enforce access restrictions, add caching, or defer resource-intensive operations, all without changing the core functionality of the real object. In our video streaming example, the proxy ensures only authorized users can access premium content, demonstrating how this pattern can provide both flexibility and security. Mastering the Proxy pattern in Kotlin can help you build more robust and scalable applications, making it a valuable addition to your design pattern toolkit.