Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Proxy Design Pattern: A Practical Implementation Guide

Tech 1

Problem Scenario

Consider an order management system with a specific business requirement: once an order is created, only the order creator should be permitted to modify the order data. All other users must be restricted from making changes.

Basic Implementation Approach

The most straightforward solution involves querying the database and comparing the order's creator identifier with the currently authenticated user's session identifier.

class Order {
    private String orderId;
    private String creatorId;
    private String productInfo;

    public Order(String orderId, String creatorId, String productInfo) {
        this.orderId = orderId;
        this.creatorId = creatorId;
        this.productInfo = productInfo;
    }

    // Getters and setters omitted for brevity
}

Service layer implementation:

class OrderService {
    private Map<String, Order> orderStore = new HashMap<>();

    public void createOrder(String orderId, String creatorId, String productInfo) {
        Order order = new Order(orderId, creatorId, productInfo);
        orderStore.put(orderId, order);
    }

    public void updateOrder(String orderId, String userId, String newInfo) {
        Order order = orderStore.get(orderId);
        
        if (order != null) {
            if (order.getCreatorId().equals(userId)) {
                order.setProductInfo(newInfo);
            } else {
                System.out.println("Access denied.");
            }
        } else {
            System.out.println("Order not found.");
        }
    }
}

The Problem with This Approach

The implementation itself is sound and represents a common pattern in Web applications often referred to as the anemic domain model, where business logic resides primarily in service layers through extensive conditional checks. However, as order-related operations grow more complex, the service code becomes increasingly bloated. Initial requirements might only ask for description updates, then product name changes, followed by granular permission rules—and each addition compounds the complexity of the updateOrder method with more conditional branches and setter invocations. This makes the codebase difficult to maintain and extend gracefully.

Proxy Pattern Definition

The Proxy Pattern provides a solution by introducing an intermediary that controls access to another object. This pattern uses object composition to protect or extend functionality rather than modifying the target object directly.

Pattern Structure

Subject: The interface defining the operations that both the proxy and real object must implement.

Proxy: The surrogate object that implements the same interface as the concrete target, maintaining a reference to invoke the actual object when needed. The proxy adds access control and protection logic before delegating calls.

RealSubject: The concrete implementation that performs the actual work.

Implementation:

interface Image {
    void render();
}

class RealImage implements Image {
    private String filename;

    public RealImage(String filename) {
        this.filename = filename;
        loadFromStorage(filename);
    }

    @Override
    public void render() {
        System.out.println("Rendering " + filename);
    }

    private void loadFromStorage(String filename) {
        System.out.println("Loading " + filename + " from storage.");
    }
}

interface ImageProxy extends Image {
    void render();
}

class ProxyImage implements ImageProxy {
    private RealImage realImage;
    private String filename;

    public ProxyImage(String filename) {
        this.filename = filename;
    }

    @Override
    public void render() {
        if (realImage == null) {
            realImage = new RealImage(filename);
        }
        realImage.render();
    }
}

public class PatternDemo {
    public static void main(String[] args) {
        ImageProxy proxy = new ProxyImage("photo.jpg");
        proxy.render();
    }
}

Practical Implementation

When dealing with order objects, we need to control external access—permitting only authorized users while blocking others. The proxy pattern accomplishes this by wrapping the Order object with an additional layer that handles permission verification. This represents a protective proxy approach.

First, define the order operation interface:

public interface OrderServiceInterface {
    String getId();
    String getProductName();
    String getDetails();
    String getCreatedBy();
    void setId(String id);
    void setDetails(String details);
    void setProductName(String name);
    void setCreatedBy(String createdBy);
}

The concrete order entity serving as the target object:

class Order implements OrderServiceInterface {
    private String id;
    private String productName;
    private String details;
    private String createdBy;

    public Order(String id, String productName, String details, String createdBy) {
        this.id = id;
        this.productName = productName;
        this.details = details;
        this.createdBy = createdBy;
    }

    @Override
    public String getId() { return id; }

    @Override
    public String getProductName() { return productName; }

    @Override
    public String getDetails() { return details; }

    @Override
    public String getCreatedBy() { return createdBy; }

    @Override
    public void setId(String id) { this.id = id; }

    @Override
    public void setProductName(String productName) { this.productName = productName; }

    @Override
    public void setCreatedBy(String createdBy) { this.createdBy = createdBy; }

    @Override
    public void setDetails(String details) { this.details = details; }
}

The proxy implementation:

class OrderProxy implements OrderServiceInterface {
    private Order order;
    private String currentUserId;

    public OrderProxy(Order order, String currentUserId) {
        this.order = order;
        this.currentUserId = currentUserId;
    }

    @Override
    public String getId() {
        return order.getId();
    }

    @Override
    public String getProductName() {
        return order.getProductName();
    }

    @Override
    public String getDetails() {
        return order.getDetails();
    }

    @Override
    public String getCreatedBy() {
        return order.getCreatedBy();
    }

    @Override
    public void setId(String id) {
        if (isAuthorized()) {
            order.setId(id);
        } else {
            throw new SecurityException("Only the creator can modify the order ID.");
        }
    }

    @Override
    public void setProductName(String productName) {
        if (isAuthorized()) {
            order.setProductName(productName);
        } else {
            throw new SecurityException("Only the creator can modify the product name.");
        }
    }

    @Override
    public void setCreatedBy(String createdBy) {
        throw new UnsupportedOperationException("Changing creator ID is not permitted.");
    }

    @Override
    public void setDetails(String details) {
        if (isAuthorized()) {
            order.setDetails(details);
        } else {
            throw new SecurityException("Only the creator can modify the order details.");
        }
    }

    private boolean isAuthorized() {
        return order.getCreatedBy() != null 
            && order.getCreatedBy().equals(currentUserId);
    }
}

Understanding the Proxy Pattern

Characteristics and Classification

The essence of the proxy pattern lies in controlling access to objects. By positioning the proxy between the client and target object, it introduces an intermediary layer that creates substantial flexibility. The proxy can execute additional operations before or after invoking the target object, enabling new functionality or extending existing capabilities. More significantly, the proxy can choose not to instantiate or invoke the target object at all—effectively replacing or completely intercepting the target.

From an implementation perspective, the proxy pattern primarily leverages composition and delegation, which becomes evident in static proxy implementations. However, inheritance-based approaches can also work and may simplify certain scenarios. Using the permission control example, the inheritance approach would look like this:

public class Order {
    // Basic order implementation without interface
}

public class OrderProxy extends Order {
    public OrderProxy(String productName, int quantity, String orderUser) {
        super(productName, quantity, orderUser);
    }

    public void setProductName(String productName, String user) {
        if (user == null || user.equals(this.getOrderUser())) {
            super.setProductName(productName);
        } else {
            System.out.println("Access denied: " + user + " cannot modify the product name.");
        }
    }

    public void setQuantity(int quantity, String user) {
        if (user == null || user.equals(this.getOrderUser())) {
            super.setQuantity(quantity);
        } else {
            System.out.println("Access denied: " + user + " cannot modify the quantity.");
        }
    }

    public void setOrderUser(String orderUser, String user) {
        if (user == null || user.equals(this.getOrderUser())) {
            super.setOrderUser(orderUser);
        } else {
            System.out.println("Access denied: " + user + " cannot modify the order user.");
        }
    }
}

Proxy Types

The intermediary layer serves different purposes depending on the proxy type:

Remote Proxy: Hides the fact that an object exists in a different address space. Clients interact with the proxy without needing to know the object's location or network details.

Virtual Proxy: Creates expensive objects on demand, delaying initialization until actually needed. This optimizes performance and conserves resources.

Protection Proxy: Adds access control and business logic around object operatiosn without modifying the target object. This is ideal for encapsulating permission rules and cross-cutting concerns.

Smart Reference: Performs additional operations when an object is accessed, such as reference counting, logging, or lazy loading.

Among these, protection proxies and remote proxies are most commonly used. The order example demonstrates a protection proxy where all business logic remains unchanged while permission handling concentrates in the proxy, effectively isolating volatile requirements and facilitating future extensions.

When to Use the Proxy Pattern

Consider the proxy pattern in these situations:

  • Use remote proxy when you need a local representative for an object in a different address space
  • Use virtual proxy when object creation is computationally expensiev
  • Use protection proxy when you need to control access to the original object
  • Use smart reference代理 when additional operations must accompany object access

Java's Built-in Proxy Support

Java provides native proxy support through the java.lang.reflect package, offering a Proxy class and InvocationHandler interface. The manual implementation described above represents static proxying—a significant drawback being that any interface changes require modifications to both the proxy and target classes.

Java's dynamic proxy resolves this limitation. While static proxies must implement every method from the interface, dynamic proxies require only a single invoke method, allowing the proxy to handle interface evolution without changes.

Dynamic proxies in Java can only delegate to interfaces (not classes) and rely on reflection combined with runtime bytecode generation. For class-level proxying, consider cglib or similar libraries.

Here's how to implement the protection proxy using Java's dynamic proxy mechanism:

import java.lang.reflect.*;

public class DynamicProxy implements InvocationHandler {
    private OrderServiceInterface targetOrder;

    public OrderServiceInterface createProxy(Order order, String userId) {
        this.targetOrder = order;
        
        return (OrderServiceInterface) Proxy.newProxyInstance(
            order.getClass().getClassLoader(),
            order.getClass().getInterfaces(),
            this);
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getName().startsWith("set")) {
            String requestingUser = (String) args[0];
            if (targetOrder.getCreatedBy() != null 
                && targetOrder.getCreatedBy().equals(requestingUser)) {
                return method.invoke(targetOrder, args);
            } else {
                System.out.println("Access denied: " + requestingUser 
                    + " cannot modify this order data");
            }
        } else {
            return method.invoke(targetOrder, args);
        }
        return null;
    }
}

Usage:

public class Client {
    public static void main(String[] args) {
        Order order = new Order("ORD-001", 100, "Alice");
        
        DynamicProxy proxyHandler = new DynamicProxy();
        OrderServiceInterface orderApi = proxyHandler.createProxy(order, "Alice");

        orderApi.setQuantity(150, "Bob");
        System.out.println("After Bob's attempt: " + order);

        orderApi.setQuantity(150, "Alice");
        System.out.println("After Alice's update: " + order);
    }
}

The proxy pattern is extensively used throughout Java ecosystems, particularly in Spring's AOP framework, which fundamentally operates on proxy-based mechanisms.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.