DEV Community

Cover image for Kotlin Design Patterns: Simplifying the Proxy Pattern
Lucas Fugisawa
Lucas Fugisawa

Posted on • Edited on • Originally published at fugisawa.com

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.

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:

  1. Lazy Initialization (Virtual Proxy): Delays the creation of expensive objects until necessary, saving resources.

  2. Access Control (Protection Proxy): Manages access to an object, allowing or denying operations based on permission checks.

  3. Remote Object Access (Remote Proxy): Represents an object in a different location, handling communication in networked applications.

  4. Logging Requests (Logging Proxy): Records operations on an object, useful for debugging and monitoring.

  5. 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";
    }
}
Enter fullscreen mode Exit fullscreen mode

In Kotlin, we would have this equivalent code:

interface DatabaseQuery {
    fun execute(): String
}

class RealDatabaseQuery : DatabaseQuery {
    override fun execute() = "Query Result"
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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";
    }
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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)