Skip to content

Proxy

Provide a surrogate or placeholder for another object to control access to it.

The Proxy pattern, similar to the Adapter pattern, offers a way to control access to another object by providing a proxy. Let's first review the Adapter pattern, which is used to convert interface A to interface B:

java
public class BAdapter implements B {
    private A a;
    
    public BAdapter(A a) {
        this.a = a;
    }
    
    public void b() {
        a.a();
    }
}

In contrast, the Proxy pattern does not convert interface A to interface B. Instead, it still conforms to interface A:

java
public class AProxy implements A {
    private A a;
    
    public AProxy(A a) {
        this.a = a;
    }
    
    public void a() {
        this.a.a();
    }
}

At first glance, it might seem pointless to wrap interface A with a proxy that also implements interface A. However, the usefulness of the Proxy pattern becomes evident when we enhance the method implementations. For example:

java
public void a() {
    if (getCurrentUser().isRoot()) {
        this.a.a();
    } else {
        throw new SecurityException("Forbidden");
    }
}

By adding additional code before and after the call to a.a(), we can implement access control. Only users who meet certain criteria (e.g., being a root user) can invoke the target method; otherwise, an exception is thrown.

Why Not Add Access Control Directly to the Target Class?

According to design principles:

  • Single Responsibility: A class should have only one responsibility.
  • Ease of Testing: Each functionality should be tested independently.

Using a Proxy to implement access control keeps the codebase cleaner and more maintainable:

  • A Interface: Defines the interface.
  • ABusiness Class: Implements the business logic for interface A.
  • APermissionProxy Class: Implements the proxy for access control.

If we want to add other types of proxies, such as ALogProxy, we can do so without modifying the existing A interface or ABusiness class.

Common Uses of the Proxy Pattern

Beyond access control, the Proxy pattern is widely used in various scenarios:

  1. Remote Proxy: Represents an object in a different address space. For example, Java's RMI (Remote Method Invocation) is a form of Remote Proxy that handles communication between local and remote objects.

  2. Virtual Proxy: Defers the creation and initialization of expensive objects until they are actually needed. For instance, a JDBC connection pool might return a proxy that doesn't establish a real database connection until a SQL operation is performed.

  3. Protection Proxy: Controls access to the original object, often used for authorization purposes.

  4. Smart Reference: Adds additional actions when an object is accessed, such as reference counting for memory management.

Example: Implementing a JDBC Connection Pool with the Proxy Pattern

Let's implement a virtual proxy for a JDBC connection pool (DataSource). The proxy will only establish a real database connection when a SQL operation is performed.

Step 1: Abstract Connection Proxy

First, create an abstract proxy class that implements the Connection interface:

java
public abstract class AbstractConnectionProxy implements Connection {

    // Abstract method to get the real Connection:
    protected abstract Connection getRealConnection();

    // Implement each method of the Connection interface:
    public Statement createStatement() throws SQLException {
        return getRealConnection().createStatement();
    }

    public PreparedStatement prepareStatement(String sql) throws SQLException {
        return getRealConnection().prepareStatement(sql);
    }

    // ... other proxy methods ...
}

This abstract class delegates all Connection interface methods to the real connection, which will be provided by subclasses.

Step 2: Lazy Connection Proxy

Next, implement the LazyConnectionProxy that delays the creation of the actual Connection until necessary:

java
public class LazyConnectionProxy extends AbstractConnectionProxy {
    private Supplier<Connection> supplier;
    private Connection target = null;

    public LazyConnectionProxy(Supplier<Connection> supplier) {
        this.supplier = supplier;
    }

    // Override the close method: only close if the target is not null
    public void close() throws SQLException {
        if (target != null) {
            System.out.println("Close connection: " + target);
            super.close();
        }
    }

    @Override
    protected Connection getRealConnection() {
        if (target == null) {
            target = supplier.get();
        }
        return target;
    }
}

If no SQL operations are performed, the target remains null, and no real connection is established. Only when a method like prepareStatement() is called does the proxy create the actual connection.

Step 3: Lazy DataSource

Finally, create a LazyDataSource that uses the LazyConnectionProxy:

java
public class LazyDataSource implements DataSource {
    private String url;
    private String username;
    private String password;

    public LazyDataSource(String url, String username, String password) {
        this.url = url;
        this.username = username;
        this.password = password;
    }

    public Connection getConnection(String username, String password) throws SQLException {
        return new LazyConnectionProxy(() -> {
            try {
                Connection conn = DriverManager.getConnection(url, username, password);
                System.out.println("Open connection: " + conn);
                return conn;
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        });
    }

    // ... other DataSource methods ...
}

Usage Example

java
DataSource lazyDataSource = new LazyDataSource(jdbcUrl, jdbcUsername, jdbcPassword);
System.out.println("get lazy connection...");
try (Connection conn1 = lazyDataSource.getConnection()) {
    // No actual Connection is opened
}
System.out.println("get lazy connection...");
try (Connection conn2 = lazyDataSource.getConnection()) {
    try (PreparedStatement ps = conn2.prepareStatement("SELECT * FROM students")) { // Real Connection is opened here
        try (ResultSet rs = ps.executeQuery()) {
            while (rs.next()) {
                System.out.println(rs.getString("name"));
            }
        }
    }
}

Output:

get lazy connection...
get lazy connection...
Open connection: com.mysql.jdbc.JDBC4Connection@7a36aefa
Tom
Alice
Grace
Close connection: com.mysql.jdbc.JDBC4Connection@7a36aefa

In the first getConnection() call, no real JDBC Connection is opened. Only in the second call, when a SQL operation is performed, is the actual connection established.

Proxy vs. Decorator

While the Proxy and Decorator patterns look similar, their intent differs:

  • Decorator: Allows adding responsibilities to objects dynamically. It lets you compose behaviors by wrapping objects with additional functionality.

  • Proxy: Controls access to an object, acting as a surrogate or placeholder. The client is unaware that it is interacting with a proxy rather than the actual object.

Key Difference: The Decorator pattern allows clients to create and combine decorators as needed, enhancing flexibility. In contrast, the Proxy pattern ensures that clients interact with the proxy without managing the underlying object directly.

Exercise

Implement a JDBC connection pool using the Proxy pattern.

Summary

The Proxy pattern encapsulates an existing interface and returns the same interface type to the caller, allowing the caller to enhance certain functionalities (e.g., authorization, lazy loading, connection pooling) without altering existing code. Using the Proxy pattern requires that the caller holds an interface, and the Proxy class must implement the same interface type.

Proxy has loaded