The Proxy Pattern is a structural design pattern in object-oriented programming. It provides a surrogate or placeholder for another object to control access to it. This pattern creates a 'proxy' object that acts as an intermediary between the client and the real object.
This pattern is useful when direct access to an object is impractical or impossible. For example: when an object is remote, expensive to create, or needs additional security.
It's widely used in software development for several key purposes. The main use cases are:
Lazy Initialization (Virtual Proxy): Delays the creation of expensive objects until necessary, saving resources.
Access Control (Protection Proxy): Manages access to an object, allowing or denying operations based on permission checks.
Remote Object Access (Remote Proxy): Represents an object in a different location, handling communication in networked applications.
Logging Requests (Logging Proxy): Records operations on an object, useful for debugging and monitoring.
Caching: Stores results of operations, returning cached data for repeated requests to enhance performance.
Let's dive into two of those use cases. In these examples, we will not consider coupling, orthogonality and best practices because we want to focus on the pattern itself.
Lazy Initialization (Virtual Proxy) example
The base classes:
Let's consider a very simple and hypothetic DatabaseQuery
interface and its implementation.
The Java solution is usually like this:
interface DatabaseQuery {
String execute();
}
class RealDatabaseQuery implements DatabaseQuery {
public String execute() {
return "Query Result";
}
}
In Kotlin, we would have this equivalent code:
interface DatabaseQuery {
fun execute(): String
}
class RealDatabaseQuery : DatabaseQuery {
override fun execute() = "Query Result"
}
The proxy class:
The following Java class allows for a lazy DatabaseQuery
that will instantiate the RealDatabaseQuery
only when (and if) needed - that is: when execute()
is called.
class LazyDatabaseQuery implements DatabaseQuery {
private RealDatabaseQuery realQuery = null;
public String execute() {
if (realQuery == null) {
realQuery = new RealDatabaseQuery();
}
return realQuery.execute();
}
}
In Kotlin, you can use lazy initialization feature to simplify the implementation of the Lazy Initialization Proxy:
class LazyDatabaseQuery : DatabaseQuery {
private val realQuery by lazy { RealDatabaseQuery() }
override fun execute() = realQuery.execute()
}
The lazy
keyword is used for lazy initialization of RealDatabaseQuery
. This ensures that RealDatabaseQuery
is only created when it's first needed, adhering to the principles of the Lazy Initialization Proxy pattern.
Logging Requests (Logging Proxy) example
This proxy logs each action performed on an object. It's useful for monitoring activities, debugging, or auditing system usage.
The base classes:
Let's define an interface DataService
with a method fetchData()
. Wel'll also define RealDataService
, a class implementing this interface, where fetchData()
would fetch and return data from a data source.
public interface DataService {
String fetchData();
String fetchStatistics();
}
public class RealDataService implements DataService {
public String fetchData() {
return "Data from service";
}
public String fetchStatistics() {
return "999";
}
}
In Kotlin, we would have this equivalent code:
interface DataService {
fun fetchData(): String
fun fetchStatistics(): String
}
class RealDataService : DataService {
override fun fetchData() = "Data from service"
override fun fetchStatistics(): String = "999"
}
The proxy class:
The LoggingDataServiceProxy
Java class in implements a proxy for DataService
. It logs a message each time its fetchData
method is called, then delegates the actual data fetching to the encapsulated DataService
instance.
public class LoggingDataServiceProxy implements DataService {
private DataService service;
public LoggingDataServiceProxy(DataService service) { this.service = service }
public String fetchData() {
System.out.println("Fetching data...");
return service.fetchData();
}
public String fetchStatistics() {
return service.fetchData();
}
}
In Kotlin, we can make use of class delegation to easily intercept and log only the desired methods:
class LoggingDataServiceProxy(private val realService: DataService) : DataService by realService {
override fun fetchData(): String {
println("LOG: Fetching data...")
return realService.fetchData()
}
}
Kotlin's class delegation (by realService
) simplifies forwarding calls while allowing override for logging.
Final Thoughts
In Kotlin, the Proxy Pattern is enhanced (or even made unnecessary) by language features like class delegation, delegated properties and lazy initialization. These features allow for more concise and expressive implementations.
--
This article was originally posted to my Lucas Fugisawa on Kotlin blog, at: https://fugisawa.com/kotlin-design-patterns-simplifying-the-proxy-pattern/
To explore more about design patterns and other Kotlin-related topics, subscribe to my newsletter on https://fugisawa.com/ and stay tuned for more insights and updates.
Top comments (0)