DEV Community

Cover image for Proxy Pattern: A Practical Guide to Smarter Object Handling
Roberto Umbelino
Roberto Umbelino

Posted on

Proxy Pattern: A Practical Guide to Smarter Object Handling

The Proxy Pattern is an object-oriented programming concept that acts as a “substitute” or “representative” for another object. This pattern is very useful when we need to control access to an object or add extra functionalities without directly modifying its code. Basically, the Proxy acts as an intermediary between the client (who uses the object) and the real object, allowing the Proxy to manage this access in various ways.

Why Use the Proxy Pattern?

Imagine you have an object that performs a heavy or time-consuming task, like loading data from a server. It might not be efficient or necessary to do this every time the object is accessed. With a Proxy, you can implement “lazy loading,” loading the data only when it’s really needed. Another common application is implementing security, where the Proxy can check permissions before allowing access to sensitive methods of the real object.

⚙️ How Does the Proxy Work?

The Proxy Pattern involves three main components:

  1. Real Object: The object that contains the main logic or data.
  2. Proxy: The intermediary that controls access to the real object.
  3. Client: The entity that interacts with the Proxy instead of directly with the real object.

When the client requests something from the Proxy, it decides whether to forward the request to the real object, handle the request itself, or even deny access.

Let’s look at two practical examples to understand this better.

💾 Example 1: Caching with Proxy

First, we’ll define an object that simulates a database. Then, we’ll create a Proxy to cache the results of queries to this database.

🏗️ Step 1: Defining the Database Object

Let’s create an object that simulates a database with user data.

const database = {
  users: {
    1: { id: 1, name: "Alice" },
    2: { id: 2, name: "Bob" },
    3: { id: 3, name: "Charlie" },
  },
  getUser(id) {
    console.log(`Fetching user with id ${id} from database...`)
    return this.users[id]
  },
}
Enter fullscreen mode Exit fullscreen mode
🛠️ Step 2: Creating the Proxy for Caching

Now, we’ll create a Proxy that caches the results of database queries.

const cacheHandler = {
  cache: {},
  get(target, prop, receiver) {
    if (prop === "getUser") {
      return (id) => {
        if (!this.cache[id]) {
          this.cache[id] = target.getUser(id)
          return this.cache[id]
        }

        console.log(`Fetching user with id ${id} from cache...`)
        return this.cache[id]
      }
    }

    return Reflect.get(target, prop, receiver)
  },
}

const dbProxy = new Proxy(database, cacheHandler)
Enter fullscreen mode Exit fullscreen mode
🔄 Step 3: Using the Proxy

Now, let’s use the Proxy to access the database data and see if the caching works correctly.

console.log(dbProxy.getUser(1)) // Fetching user with id 1 from database...
console.log(dbProxy.getUser(1)) // Fetching user with id 1 from cache...
console.log(dbProxy.getUser(2)) // Fetching user with id 2 from database...
console.log(dbProxy.getUser(2)) // Fetching user with id 2 from cache...
console.log(dbProxy.getUser(3)) // Fetching user with id 3 from database...
console.log(dbProxy.getUser(3)) // Fetching user with id 3 from cache...
Enter fullscreen mode Exit fullscreen mode
📊 Result

When you run this code, you’ll see that the first query for a specific user accesses the (simulated) database, and subsequent queries for the same user access the cache, avoiding the need to query the database again. Here’s the expected output:

Fetching user with id 1 from database...
{ id: 1, name: 'Alice' }
Fetching user with id 1 from cache...
{ id: 1, name: 'Alice' }
Fetching user with id 2 from database...
{ id: 2, name: 'Bob' }
Fetching user with id 2 from cache...
{ id: 2, name: 'Bob' }
Fetching user with id 3 from database...
{ id: 3, name: 'Charlie' }
Fetching user with id 3 from cache...
{ id: 3, name: 'Charlie' }
Enter fullscreen mode Exit fullscreen mode

📝 Example 2: Form Validator with Proxy

Now, let’s create a Proxy to validate form data before it gets set on the object.

const form = {
  name: "",
  email: "",
  password: "",
}

const validator = {
  set(target, prop, value) {
    if (prop === "email" && !isValidEmail(value)) {
      throw new Error("Invalid email address")
    }

    if (prop === "password" && !isValidPassword(value)) {
      throw new Error("Password must contain at least 8 characters")
    }
    target[prop] = value
    return true
  },
}

const formProxy = new Proxy(form, validator)

function isValidEmail(email) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}

function isValidPassword(password) {
  return password.length >= 8
}

formProxy.name = "John"
formProxy.email = "john.doe@example.com" // throws an Error: "Invalid email address"
formProxy.password = "1234" // throws an Error: "Password must contain at least 8 characters"
Enter fullscreen mode Exit fullscreen mode

Here, the Proxy intercepts the value assignments to the form fields and validates them before allowing the update. If validation fails, an error is thrown, preventing invalid values from being set.

Conclusion

The Proxy Pattern is a powerful tool for adding a layer of control and functionality over an object without directly modifying its code. With it, we can implement caching, lazy loading, input validation, access control, and much more in an organized and efficient way.

Top comments (0)