Kotlin Design Patterns: Simplifying the Proxy Pattern

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.

It 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:

Java
interface DatabaseQuery { 
    String execute(); 
}

class RealDatabaseQuery implements DatabaseQuery {
    public String execute() {
        return "Query Result";
    }
}

In Kotlin, we would have this equivalent code:

Kotlin
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.

Java
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:

Kotlin
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.

Java
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:

Kotlin
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.

Java
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:

Kotlin
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.