DEV Community

Sota
Sota

Posted on

Proxy Pattern

What is Proxy Pattern?

Proxy pattern is a structural pattern that provides a surrogate or placeholder for another object to control access to it.

When to use it?

There are different types of proxies depending on its specific purpose. But essentially, Proxy pattern is providing a representative for another object.

  • Use the virtual proxy when the creation of a resource is expensive, and you want to delay its instantiation until it is actually needed.

  • Use the protection proxy when you need to control access to an object, typically based on permissions or roles.

  • Use the remote proxy when you want to represent an object located in a different address space (e.g., on a remote server) and communicate with it as if it were local.

There are more use cases of Proxy pattern. If you're interested, you can check on the internet for these proxies: firewall proxy, smart reference proxy, caching proxy, synchronization proxy, complexity hiding proxy, copy-on-write proxy, etc.

Problem

We're developing bank system. Each customer can access to any customer's account name and account number (to send money for example), but deposit, withdraw, and view balance operations should be only allowed by its account holder.
You might think we could create two classes Holder and NonHolder, then implements corresponding behavior. But notice a customer is a holder of their own bank account and non-holder of other bank account at the same time. We could implement in the way that customer can switch Holder or NonHolder at run time, but it's dangerous because now customers can be a holder of other customer's bank account.

We need a guardian to protect credential info from non-holder. Can you guess his name? That is the protection proxy!

Structure

Before going to Solution section, let's check out general (or static) proxy structure.

Image description

Subject provides a common interface for RealSubject and Proxy. In this way, Client aren't aware they are communicating with RealSubject, but they are actually interact with Proxy, that is why Proxy is known as a surrogate or placeholder. When Client calls a method on Proxy, Proxy decides whether it calls the requested method on RealSubject or does alternative operation such as rejecting the request or displaying text while loading heavy-weight resources.

Dynamic Proxy

Image description

Dynamic proxy is the proxy that allows client to instantiate proxy at runtime. Dynamic proxy can be implemented with Java API Proxy (java.lang.reflect.Proxy).

The class diagram is a bit different from general proxy structure. Let's steps through the diagram...

Proxy now consists of two classes, Proxy and InvocationHandler classes.

Client calls a method on Proxy.

Proxy passes the method called by Client to InvocationHandler.

InvocationHandler receives the method from Proxy, and decides whether it calls the actual method on RealSubject or does alternative things.

Solution

Sorry for making you waiting for a long time, let's get into the solution.
We'll implement dynamic protection proxy.

Image description

  1. Client
    Client calls a method on Proxy.

  2. IBankAccount
    Client talks to Proxy via IBankAccount interface.

  3. Proxy
    Proxy receives method calls from Client. These methods are passed to InvocationHandler.

  4. HolderInvocationHandler
    HolderInvocationHandler handles methods calls (invocations) from account holder. Account holder are allowed to access all methods on RealSubject.

  5. NonHolderInvocationHandler
    NonHolderInvocationHandler handles methods invocations from non account holder. Non account holder can't access to some of methods such as withdraw(). For example, if non holder calls withdraw() on Proxy, NonHolderInvocationHandler will receives that call, and block access to RealSubject.

Implementation in Java

public interface IBankAccount {
    // Anyone can access to account's name and number
    String getName();
    String getAccountNumber();

    // deposit, withdraw, viewBalance is only allowed by account holder
    void deposit(double amount);
    void withdraw(double amount);
    void viewBalance();
}
Enter fullscreen mode Exit fullscreen mode
public class BankAccount implements IBankAccount {

    private String name;
    private String accountNumber;
    private double balance;

    public BankAccount(String name, String accountNumber, double balance) {
        this.name = name;
        this.accountNumber = accountNumber;
        this.balance = balance;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getAccountNumber() {
        return accountNumber;
    }

    @Override
    public void deposit(double amount) {
        balance += amount;
        System.out.println(name + " deposit $" + amount);
    }

    @Override
    public void withdraw(double amount) {
        balance -= amount;
        System.out.println(name + " withdraw $" + amount);
    }

    @Override
    public void viewBalance() {
        System.out.println(name + "'s balance: $" + balance);
    }
}
Enter fullscreen mode Exit fullscreen mode
public class HolderInvocationHandler implements InvocationHandler {

    private IBankAccount account;

    public HolderInvocationHandler(IBankAccount account) {
        this.account = account;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            return method.invoke(account, args);
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }
}
Enter fullscreen mode Exit fullscreen mode
public class NonHolderInvocationHandler implements InvocationHandler {

    private IBankAccount account;

    public NonHolderInvocationHandler(IBankAccount account) {
        this.account = account;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException {
        try {
            // Anyone can access to other person's name and account number
            if (method.getName().startsWith("get")) {
                method.invoke(account, args);
            } else {
                // Other methods like deposit() are not allowed by anyone but account holder
                throw new IllegalAccessException();
            }
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }
}
Enter fullscreen mode Exit fullscreen mode
public class BankAccountTestDrive {

    public static void main(String[] args) {
        BankAccountTestDrive test = new BankAccountTestDrive();
        test.drive();
    }

    public void drive() {
        // Instantiate real subject
        IBankAccount alex = new BankAccount("Alex", "123-abc-456", 500.0);

        System.out.println("-- Testing access via holder proxy --");
        IBankAccount holderProxy = getHolderProxy(alex); // Create proxy
        // Holder proxy allows everything
        System.out.println("Account name: " + holderProxy.getName());
        System.out.println("Account number: " + holderProxy.getAccountNumber());
        holderProxy.deposit(100.0);
        holderProxy.withdraw(50.0);
        holderProxy.viewBalance();

        System.out.println("-- Testing access via non-holder proxy --");
        IBankAccount nonHolderProxy = getNonHolderProxy(alex); // Create proxy
        // Non holder proxy allows getting name and account number
        System.out.println("Account name: " + holderProxy.getName());
        System.out.println("Account number: " + holderProxy.getAccountNumber());
        // Non holder proxy doesn't allow accessing credential info
        try {
            nonHolderProxy.deposit(200.0);
        } catch (Exception e) {
            System.out.println("Can't deposit from non-holder proxy");
        }
        try {
            nonHolderProxy.withdraw(50.0);
        } catch (Exception e) {
            System.out.println("Can't withdraw from non-holder proxy");
        }
        try {
            nonHolderProxy.viewBalance();
        } catch (Exception e) {
            System.out.println("Can't view balance from non-holder proxy");
        }
    }

    // This method takes a IBankAccount object (real subject) and returns a proxy for it.
    // Because proxy has the same interface as the subject, this method returns IBankAccount.
    IBankAccount getHolderProxy(IBankAccount account) {
        // Call static method newProxyInstance on Java API Proxy
        return (IBankAccount) Proxy.newProxyInstance(
                account.getClass().getClassLoader(), // Pass class loader
                account.getClass().getInterfaces(), // Proxy needs to implement the interfaces that real subject implements
                new HolderInvocationHandler(account)); // Pass real subject into the constructor of the invocation handler.
    }

    IBankAccount getNonHolderProxy(IBankAccount account) {
        return (IBankAccount) Proxy.newProxyInstance(
                account.getClass().getClassLoader(),
                account.getClass().getInterfaces(),
                new NonHolderInvocationHandler(account));
    }
}
Enter fullscreen mode Exit fullscreen mode

Output:

-- Testing access via holder proxy --
Account name: Alex
Account number: 123-abc-456
Alex deposit $100.0
Alex withdraw $50.0
Alex's balance: $550.0
-- Testing access via non-holder proxy --
Account name: Alex
Account number: 123-abc-456
Can't deposit from non-holder proxy
Can't withdraw from non-holder proxy
Can't view balance from non-holder proxy
Enter fullscreen mode Exit fullscreen mode

Pitfalls

  • Proxy increases the number of classes and objects in our design.

Comparison with Decorator Pattern

  • Decorator and Proxy both wraps objects, but their intent is different. Decorator adds small behavior to an object, while Proxy controls access.

You can check all the design pattern implementations here.
GitHub Repository

Top comments (0)