423 lines
12 KiB
Markdown
423 lines
12 KiB
Markdown
# Proxy Pattern
|
|
|
|
> Provide a surrogate or placeholder for another object to control access to it.
|
|
|
|
## Problem
|
|
|
|
You need to control how an object is accessed. Common scenarios:
|
|
1. **Virtual proxy** — Delay expensive object creation until actually needed (lazy loading).
|
|
2. **Protection proxy** — Restrict access based on permissions.
|
|
3. **Logging proxy** — Log all interactions transparently.
|
|
|
|
## Solution
|
|
|
|
Create a proxy class with the same interface as the real object. The proxy holds a reference to the real object and adds control logic (lazy init, access checks, logging) before/after delegating calls.
|
|
|
|
```
|
|
┌────────┐ ┌──────────────────┐
|
|
│ Client │─────▶│ Service (intf) │
|
|
└────────┘ │ + request() │
|
|
└──────┬───────────┘
|
|
│
|
|
┌───────┴───────┐
|
|
│ │
|
|
┌────┴────┐ ┌─────┴─────┐
|
|
│RealServ │ │ Proxy │
|
|
│ │ │ + real │
|
|
└─────────┘ │ + request│
|
|
└──────────┘
|
|
```
|
|
|
|
## When to Use
|
|
|
|
- **Virtual proxy**: The real object is expensive to create and may not always be needed.
|
|
- **Protection proxy**: You need to check permissions before granting access.
|
|
- **Logging proxy**: You want to record interactions without modifying the real object.
|
|
- **Caching proxy**: You want to cache results of expensive operations.
|
|
|
|
## When to Avoid
|
|
|
|
- The added indirection is unnecessary for simple, inexpensive objects.
|
|
- The proxy would duplicate logic that belongs in the real object.
|
|
- You need to proxy many methods and a language-level mechanism (e.g., Python `__getattr__`, JS `Proxy`) would be cleaner.
|
|
|
|
## Pseudocode
|
|
|
|
```
|
|
interface Database:
|
|
method query(sql): string
|
|
|
|
class RealDatabase implements Database:
|
|
constructor():
|
|
output "Connecting to database... (expensive)"
|
|
method query(sql):
|
|
return "Result of: " + sql
|
|
|
|
// Virtual proxy — lazy init
|
|
class LazyDatabaseProxy implements Database:
|
|
field real: Database = null
|
|
method query(sql):
|
|
if real is null:
|
|
real = new RealDatabase()
|
|
return real.query(sql)
|
|
|
|
// Protection proxy
|
|
class ProtectedDatabaseProxy implements Database:
|
|
field real: Database
|
|
field user_role: string
|
|
constructor(real, role):
|
|
this.real = real; this.user_role = role
|
|
method query(sql):
|
|
if user_role != "admin":
|
|
output "ACCESS DENIED for role: " + user_role
|
|
return null
|
|
return real.query(sql)
|
|
|
|
// Logging proxy
|
|
class LoggingDatabaseProxy implements Database:
|
|
field real: Database
|
|
constructor(real): this.real = real
|
|
method query(sql):
|
|
output "[LOG] query: " + sql
|
|
result = real.query(sql)
|
|
output "[LOG] result: " + result
|
|
return result
|
|
```
|
|
|
|
## Python
|
|
|
|
```python
|
|
from abc import ABC, abstractmethod
|
|
|
|
|
|
class Database(ABC):
|
|
"""Service interface."""
|
|
|
|
@abstractmethod
|
|
def query(self, sql: str) -> str: ...
|
|
|
|
|
|
class RealDatabase(Database):
|
|
"""Expensive real service."""
|
|
|
|
def __init__(self) -> None:
|
|
print(" [RealDB] Connecting to database... (expensive)")
|
|
|
|
def query(self, sql: str) -> str:
|
|
return f"Result of: {sql}"
|
|
|
|
|
|
# --- Virtual Proxy (lazy loading) ---
|
|
|
|
class LazyDatabaseProxy(Database):
|
|
"""Delays creation of RealDatabase until first use."""
|
|
|
|
def __init__(self) -> None:
|
|
self._real: RealDatabase | None = None
|
|
|
|
def query(self, sql: str) -> str:
|
|
if self._real is None:
|
|
print(" [LazyProxy] First access — initializing real database")
|
|
self._real = RealDatabase()
|
|
return self._real.query(sql)
|
|
|
|
|
|
# --- Protection Proxy ---
|
|
|
|
class ProtectedDatabaseProxy(Database):
|
|
"""Checks user role before allowing queries."""
|
|
|
|
def __init__(self, real: Database, user_role: str) -> None:
|
|
self._real = real
|
|
self._role = user_role
|
|
|
|
def query(self, sql: str) -> str:
|
|
if self._role != "admin":
|
|
print(f" [ProtectionProxy] ACCESS DENIED for role '{self._role}'")
|
|
return "ACCESS DENIED"
|
|
return self._real.query(sql)
|
|
|
|
|
|
# --- Logging Proxy ---
|
|
|
|
class LoggingDatabaseProxy(Database):
|
|
"""Logs every query transparently."""
|
|
|
|
def __init__(self, real: Database) -> None:
|
|
self._real = real
|
|
|
|
def query(self, sql: str) -> str:
|
|
print(f" [LogProxy] Executing: {sql}")
|
|
result = self._real.query(sql)
|
|
print(f" [LogProxy] Got: {result}")
|
|
return result
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("=== Virtual Proxy (lazy loading) ===")
|
|
lazy_db = LazyDatabaseProxy()
|
|
print("Proxy created, no DB connection yet.")
|
|
print(lazy_db.query("SELECT * FROM users"))
|
|
print(lazy_db.query("SELECT * FROM orders"))
|
|
|
|
print("\n=== Protection Proxy ===")
|
|
real_db = RealDatabase()
|
|
admin_db = ProtectedDatabaseProxy(real_db, "admin")
|
|
guest_db = ProtectedDatabaseProxy(real_db, "guest")
|
|
print(admin_db.query("SELECT * FROM secrets"))
|
|
print(guest_db.query("SELECT * FROM secrets"))
|
|
|
|
print("\n=== Logging Proxy ===")
|
|
logged_db = LoggingDatabaseProxy(real_db)
|
|
print(logged_db.query("SELECT count(*) FROM users"))
|
|
```
|
|
|
|
**Output:**
|
|
```
|
|
=== Virtual Proxy (lazy loading) ===
|
|
Proxy created, no DB connection yet.
|
|
[LazyProxy] First access — initializing real database
|
|
[RealDB] Connecting to database... (expensive)
|
|
Result of: SELECT * FROM users
|
|
Result of: SELECT * FROM orders
|
|
|
|
=== Protection Proxy ===
|
|
[RealDB] Connecting to database... (expensive)
|
|
Result of: SELECT * FROM secrets
|
|
[ProtectionProxy] ACCESS DENIED for role 'guest'
|
|
ACCESS DENIED
|
|
|
|
=== Logging Proxy ===
|
|
[LogProxy] Executing: SELECT count(*) FROM users
|
|
[LogProxy] Got: Result of: SELECT count(*) FROM users
|
|
Result of: SELECT count(*) FROM users
|
|
```
|
|
|
|
## Go
|
|
|
|
```go
|
|
package main
|
|
|
|
import "fmt"
|
|
|
|
// Database is the service interface.
|
|
type Database interface {
|
|
Query(sql string) string
|
|
}
|
|
|
|
// RealDatabase is the expensive real service.
|
|
type RealDatabase struct{}
|
|
|
|
func NewRealDatabase() *RealDatabase {
|
|
fmt.Println(" [RealDB] Connecting to database... (expensive)")
|
|
return &RealDatabase{}
|
|
}
|
|
|
|
func (db *RealDatabase) Query(sql string) string {
|
|
return "Result of: " + sql
|
|
}
|
|
|
|
// --- Virtual Proxy ---
|
|
|
|
type LazyDatabaseProxy struct {
|
|
real *RealDatabase
|
|
}
|
|
|
|
func (p *LazyDatabaseProxy) Query(sql string) string {
|
|
if p.real == nil {
|
|
fmt.Println(" [LazyProxy] First access — initializing real database")
|
|
p.real = NewRealDatabase()
|
|
}
|
|
return p.real.Query(sql)
|
|
}
|
|
|
|
// --- Protection Proxy ---
|
|
|
|
type ProtectedDatabaseProxy struct {
|
|
real Database
|
|
role string
|
|
}
|
|
|
|
func NewProtectedProxy(real Database, role string) *ProtectedDatabaseProxy {
|
|
return &ProtectedDatabaseProxy{real: real, role: role}
|
|
}
|
|
|
|
func (p *ProtectedDatabaseProxy) Query(sql string) string {
|
|
if p.role != "admin" {
|
|
fmt.Printf(" [ProtectionProxy] ACCESS DENIED for role '%s'\n", p.role)
|
|
return "ACCESS DENIED"
|
|
}
|
|
return p.real.Query(sql)
|
|
}
|
|
|
|
// --- Logging Proxy ---
|
|
|
|
type LoggingDatabaseProxy struct {
|
|
real Database
|
|
}
|
|
|
|
func NewLoggingProxy(real Database) *LoggingDatabaseProxy {
|
|
return &LoggingDatabaseProxy{real: real}
|
|
}
|
|
|
|
func (p *LoggingDatabaseProxy) Query(sql string) string {
|
|
fmt.Printf(" [LogProxy] Executing: %s\n", sql)
|
|
result := p.real.Query(sql)
|
|
fmt.Printf(" [LogProxy] Got: %s\n", result)
|
|
return result
|
|
}
|
|
|
|
func main() {
|
|
fmt.Println("=== Virtual Proxy (lazy loading) ===")
|
|
lazyDB := &LazyDatabaseProxy{}
|
|
fmt.Println("Proxy created, no DB connection yet.")
|
|
fmt.Println(lazyDB.Query("SELECT * FROM users"))
|
|
fmt.Println(lazyDB.Query("SELECT * FROM orders"))
|
|
|
|
fmt.Println("\n=== Protection Proxy ===")
|
|
realDB := NewRealDatabase()
|
|
adminDB := NewProtectedProxy(realDB, "admin")
|
|
guestDB := NewProtectedProxy(realDB, "guest")
|
|
fmt.Println(adminDB.Query("SELECT * FROM secrets"))
|
|
fmt.Println(guestDB.Query("SELECT * FROM secrets"))
|
|
|
|
fmt.Println("\n=== Logging Proxy ===")
|
|
loggedDB := NewLoggingProxy(realDB)
|
|
fmt.Println(loggedDB.Query("SELECT count(*) FROM users"))
|
|
}
|
|
```
|
|
|
|
**Output:**
|
|
```
|
|
=== Virtual Proxy (lazy loading) ===
|
|
Proxy created, no DB connection yet.
|
|
[LazyProxy] First access — initializing real database
|
|
[RealDB] Connecting to database... (expensive)
|
|
Result of: SELECT * FROM users
|
|
Result of: SELECT * FROM orders
|
|
|
|
=== Protection Proxy ===
|
|
[RealDB] Connecting to database... (expensive)
|
|
Result of: SELECT * FROM secrets
|
|
[ProtectionProxy] ACCESS DENIED for role 'guest'
|
|
ACCESS DENIED
|
|
|
|
=== Logging Proxy ===
|
|
[LogProxy] Executing: SELECT count(*) FROM users
|
|
[LogProxy] Got: Result of: SELECT count(*) FROM users
|
|
Result of: SELECT count(*) FROM users
|
|
```
|
|
|
|
## JavaScript
|
|
|
|
```javascript
|
|
class RealDatabase {
|
|
constructor() {
|
|
console.log(" [RealDB] Connecting to database... (expensive)");
|
|
}
|
|
|
|
query(sql) {
|
|
return `Result of: ${sql}`;
|
|
}
|
|
}
|
|
|
|
// --- Virtual Proxy (lazy loading) ---
|
|
|
|
class LazyDatabaseProxy {
|
|
constructor() {
|
|
this._real = null;
|
|
}
|
|
|
|
query(sql) {
|
|
if (this._real === null) {
|
|
console.log(
|
|
" [LazyProxy] First access — initializing real database"
|
|
);
|
|
this._real = new RealDatabase();
|
|
}
|
|
return this._real.query(sql);
|
|
}
|
|
}
|
|
|
|
// --- Protection Proxy ---
|
|
|
|
class ProtectedDatabaseProxy {
|
|
constructor(real, role) {
|
|
this._real = real;
|
|
this._role = role;
|
|
}
|
|
|
|
query(sql) {
|
|
if (this._role !== "admin") {
|
|
console.log(
|
|
` [ProtectionProxy] ACCESS DENIED for role '${this._role}'`
|
|
);
|
|
return "ACCESS DENIED";
|
|
}
|
|
return this._real.query(sql);
|
|
}
|
|
}
|
|
|
|
// --- Logging Proxy ---
|
|
|
|
class LoggingDatabaseProxy {
|
|
constructor(real) {
|
|
this._real = real;
|
|
}
|
|
|
|
query(sql) {
|
|
console.log(` [LogProxy] Executing: ${sql}`);
|
|
const result = this._real.query(sql);
|
|
console.log(` [LogProxy] Got: ${result}`);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// --- Main ---
|
|
console.log("=== Virtual Proxy (lazy loading) ===");
|
|
const lazyDB = new LazyDatabaseProxy();
|
|
console.log("Proxy created, no DB connection yet.");
|
|
console.log(lazyDB.query("SELECT * FROM users"));
|
|
console.log(lazyDB.query("SELECT * FROM orders"));
|
|
|
|
console.log("\n=== Protection Proxy ===");
|
|
const realDB = new RealDatabase();
|
|
const adminDB = new ProtectedDatabaseProxy(realDB, "admin");
|
|
const guestDB = new ProtectedDatabaseProxy(realDB, "guest");
|
|
console.log(adminDB.query("SELECT * FROM secrets"));
|
|
console.log(guestDB.query("SELECT * FROM secrets"));
|
|
|
|
console.log("\n=== Logging Proxy ===");
|
|
const loggedDB = new LoggingDatabaseProxy(realDB);
|
|
console.log(loggedDB.query("SELECT count(*) FROM users"));
|
|
```
|
|
|
|
**Output:**
|
|
```
|
|
=== Virtual Proxy (lazy loading) ===
|
|
Proxy created, no DB connection yet.
|
|
[LazyProxy] First access — initializing real database
|
|
[RealDB] Connecting to database... (expensive)
|
|
Result of: SELECT * FROM users
|
|
Result of: SELECT * FROM orders
|
|
|
|
=== Protection Proxy ===
|
|
[RealDB] Connecting to database... (expensive)
|
|
Result of: SELECT * FROM secrets
|
|
[ProtectionProxy] ACCESS DENIED for role 'guest'
|
|
ACCESS DENIED
|
|
|
|
=== Logging Proxy ===
|
|
[LogProxy] Executing: SELECT count(*) FROM users
|
|
[LogProxy] Got: Result of: SELECT count(*) FROM users
|
|
Result of: SELECT count(*) FROM users
|
|
```
|
|
|
|
## Related Patterns
|
|
|
|
- **Decorator** — Both wrap objects with the same interface. Proxy controls access; Decorator adds behavior. Combined: Decorator wraps a Proxy to add both behavior and access control.
|
|
- **Adapter** — Adapter wraps to change the interface; Proxy wraps to control access while keeping the same interface.
|
|
- **Facade** — Facade simplifies a subsystem interface; Proxy keeps the same interface but adds control.
|
|
- **Memoization** — Caching Proxy implements memoization transparently.
|
|
- **Guard Clause** — Protection Proxy implements Guard Clause logic at the wrapper level.
|