Introduction

The Proxy design pattern is a structural pattern that provides a surrogate or placeholder for another object to control access to it. This pattern is particularly useful when you need to add an additional level of control over an object, such as lazy initialization, access control, logging, or caching.

Key Concepts

  • Proxy Object: Acts as an intermediary between the client and the real object.
  • Real Subject: The actual object that the proxy represents.
  • Client: The entity that interacts with the proxy to perform operations on the real subject.

Types of Proxies

  1. Virtual Proxy: Controls access to a resource that is expensive to create. It creates the resource only when it is needed.
  2. Remote Proxy: Represents an object that exists in a different address space. It handles communication between the client and the remote object.
  3. Protection Proxy: Controls access to the real object by checking permissions.
  4. Smart Proxy: Adds additional behavior when an object is accessed, such as reference counting or logging.

Structure

The Proxy pattern involves the following participants:

  • Subject: Defines the common interface for RealSubject and Proxy.
  • RealSubject: Implements the Subject interface and represents the actual object.
  • Proxy: Implements the Subject interface and controls access to the RealSubject.

UML Diagram

+---------+        +---------+        +---------+
|  Client |------->|  Proxy  |------->|RealSubject|
+---------+        +---------+        +---------+

Example

Let's consider an example where we use a Virtual Proxy to control access to a resource-intensive object.

Step-by-Step Implementation

  1. Define the Subject Interface:
public interface Image {
    void display();
}
  1. Implement the RealSubject:
public class RealImage implements Image {
    private String fileName;

    public RealImage(String fileName) {
        this.fileName = fileName;
        loadFromDisk(fileName);
    }

    private void loadFromDisk(String fileName) {
        System.out.println("Loading " + fileName);
    }

    @Override
    public void display() {
        System.out.println("Displaying " + fileName);
    }
}
  1. Implement the Proxy:
public class ProxyImage implements Image {
    private RealImage realImage;
    private String fileName;

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

    @Override
    public void display() {
        if (realImage == null) {
            realImage = new RealImage(fileName);
        }
        realImage.display();
    }
}
  1. Client Code:
public class ProxyPatternDemo {
    public static void main(String[] args) {
        Image image = new ProxyImage("test_image.jpg");

        // Image will be loaded from disk
        image.display();
        System.out.println("");

        // Image will not be loaded from disk
        image.display();
    }
}

Explanation

  • The Image interface defines the common interface for RealImage and ProxyImage.
  • The RealImage class implements the Image interface and represents the actual object that is resource-intensive to create.
  • The ProxyImage class also implements the Image interface and controls access to the RealImage object. It creates the RealImage object only when it is needed.
  • The ProxyPatternDemo class demonstrates the use of the Proxy pattern. The first call to display() loads the image from disk, while the second call does not, as the image is already loaded.

Practical Exercises

Exercise 1: Implement a Protection Proxy

Task: Implement a Protection Proxy that controls access to a BankAccount object based on user roles.

  1. Define the Subject Interface:
public interface BankAccount {
    void deposit(double amount);
    void withdraw(double amount);
    double getBalance();
}
  1. Implement the RealSubject:
public class RealBankAccount implements BankAccount {
    private double balance;

    @Override
    public void deposit(double amount) {
        balance += amount;
    }

    @Override
    public void withdraw(double amount) {
        if (balance >= amount) {
            balance -= amount;
        } else {
            System.out.println("Insufficient funds");
        }
    }

    @Override
    public double getBalance() {
        return balance;
    }
}
  1. Implement the Proxy:
public class BankAccountProxy implements BankAccount {
    private RealBankAccount realBankAccount;
    private String userRole;

    public BankAccountProxy(String userRole) {
        this.realBankAccount = new RealBankAccount();
        this.userRole = userRole;
    }

    @Override
    public void deposit(double amount) {
        if ("Admin".equals(userRole)) {
            realBankAccount.deposit(amount);
        } else {
            System.out.println("Access Denied: Only Admin can deposit");
        }
    }

    @Override
    public void withdraw(double amount) {
        if ("Admin".equals(userRole)) {
            realBankAccount.withdraw(amount);
        } else {
            System.out.println("Access Denied: Only Admin can withdraw");
        }
    }

    @Override
    public double getBalance() {
        return realBankAccount.getBalance();
    }
}
  1. Client Code:
public class ProtectionProxyDemo {
    public static void main(String[] args) {
        BankAccount adminAccount = new BankAccountProxy("Admin");
        BankAccount userAccount = new BankAccountProxy("User");

        adminAccount.deposit(1000);
        userAccount.deposit(1000);

        adminAccount.withdraw(500);
        userAccount.withdraw(500);

        System.out.println("Admin Account Balance: " + adminAccount.getBalance());
        System.out.println("User Account Balance: " + userAccount.getBalance());
    }
}

Solution Explanation

  • The BankAccount interface defines the common interface for RealBankAccount and BankAccountProxy.
  • The RealBankAccount class implements the BankAccount interface and represents the actual bank account.
  • The BankAccountProxy class also implements the BankAccount interface and controls access to the RealBankAccount object based on user roles.
  • The ProtectionProxyDemo class demonstrates the use of the Protection Proxy. The Admin role can deposit and withdraw money, while the User role cannot.

Common Mistakes and Tips

  • Mistake: Not implementing the same interface in both the Proxy and RealSubject.

    • Tip: Ensure that both the Proxy and RealSubject implement the same interface to maintain consistency.
  • Mistake: Creating the RealSubject object in the Proxy constructor.

    • Tip: Use lazy initialization to create the RealSubject object only when it is needed.
  • Mistake: Overcomplicating the Proxy logic.

    • Tip: Keep the Proxy logic simple and focused on controlling access to the RealSubject.

Conclusion

The Proxy design pattern is a powerful tool for controlling access to objects. By understanding and implementing different types of proxies, such as Virtual, Remote, Protection, and Smart proxies, you can add an additional layer of control and functionality to your applications. Practice implementing proxies in various scenarios to reinforce your understanding of this pattern.

© Copyright 2024. All rights reserved