Proxy Pattern: A Practical Guide to Smarter Object Handling
Roberto Umbelino
Posted on June 21, 2024
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:
- Real Object: The object that contains the main logic or data.
- Proxy: The intermediary that controls access to the real object.
- 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]
},
}
🛠️ 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)
🔄 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...
📊 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' }
📝 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"
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.
Posted on June 21, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.