Chuyển tới nội dung chính

Proxy Pattern (Mẫu Proxy)

Định nghĩa (Definition)

Proxy cung cấp thay thế hoặc placeholder (Substitute or Placeholder) cho object khác để kiểm soát truy cập đến nó. Proxy implement cùng interface với object thật và chuyển tiếp request đến nó, thêm logic kiểm soát trước hoặc sau khi ủy quyền (Delegation).

Ví dụ Coffee Shop

Quán cà phê có CoffeeMachine đắt tiền cần thời gian khởi tạo (làm nóng nồi hơi (Boiler), hiệu chuẩn áp suất). Chúng ta dùng các proxy khác nhau để:

  1. Virtual Proxy — lazy-load máy đắt tiền chỉ khi cần lần đầu
  2. Protection Proxy — giới hạn truy cập máy chỉ cho barista được ủy quyền
  3. Caching Proxy — cache kết quả pha gần đây để tránh pha lại đơn giống hệt

Cấu trúc (Structure)

Subject Interface

public record Beverage(string Coffee, CupSize Size, DateTime BrewedAt);

public interface ICoffeeMachine
{
Beverage Brew(string coffee, CupSize size);
bool IsReady();
}

Real Subject — Khởi tạo tốn kém (Expensive to Create)

public class RealCoffeeMachine : ICoffeeMachine
{
private bool _initialized;

public RealCoffeeMachine()
{
// Khởi tạo tốn kém — mô phỏng 2 giây startup
Console.WriteLine(" [RealCoffeeMachine] Đang làm nóng nồi hơi...");
Console.WriteLine(" [RealCoffeeMachine] Đang hiệu chuẩn áp suất...");
Console.WriteLine(" [RealCoffeeMachine] Máy sẵn sàng!");
_initialized = true;
}

public Beverage Brew(string coffee, CupSize size)
{
Console.WriteLine($" [RealCoffeeMachine] Đang pha {size} {coffee}...");
return new Beverage(coffee, size, DateTime.UtcNow);
}

public bool IsReady() => _initialized;
}

Virtual Proxy — Lazy Loading

public class VirtualProxy : ICoffeeMachine
{
private RealCoffeeMachine? _real;

public Beverage Brew(string coffee, CupSize size)
{
// Chỉ tạo máy thật khi cần lần đầu
_real ??= new RealCoffeeMachine();
return _real.Brew(coffee, size);
}

public bool IsReady() => _real?.IsReady() ?? false;
}

Protection Proxy — Kiểm soát truy cập (Access Control)

public class ProtectionProxy : ICoffeeMachine
{
private readonly ICoffeeMachine _inner;
private readonly string _currentUser;
private readonly HashSet<string> _authorizedUsers;

public ProtectionProxy(ICoffeeMachine inner, string currentUser,
HashSet<string> authorizedUsers)
{
_inner = inner;
_currentUser = currentUser;
_authorizedUsers = authorizedUsers;
}

public Beverage Brew(string coffee, CupSize size)
{
if (!_authorizedUsers.Contains(_currentUser))
throw new UnauthorizedAccessException(
$"{_currentUser} không có quyền sử dụng máy này.");

Console.WriteLine($" [ProtectionProxy] Cấp quyền cho {_currentUser}");
return _inner.Brew(coffee, size);
}

public bool IsReady() => _inner.IsReady();
}

Caching Proxy — Memoization

public class CachingProxy : ICoffeeMachine
{
private readonly ICoffeeMachine _inner;
private readonly Dictionary<string, Beverage> _cache = new();

public CachingProxy(ICoffeeMachine inner)
{
_inner = inner;
}

public Beverage Brew(string coffee, CupSize size)
{
var key = $"{coffee}:{size}";

if (_cache.TryGetValue(key, out var cached))
{
Console.WriteLine($" [CachingProxy] Cache hit cho {key}");
return cached with { BrewedAt = DateTime.UtcNow };
}

Console.WriteLine($" [CachingProxy] Cache miss cho {key}");
var beverage = _inner.Brew(coffee, size);
_cache[key] = beverage;
return beverage;
}

public bool IsReady() => _inner.IsReady();
}

Client — Xếp chồng Proxy (Stacking Proxies)

// Tạo máy khi cần → kiểm tra quyền → cache kết quả
// Client chỉ thấy ICoffeeMachine
var authorizedBaristas = new HashSet<string> { "Alice", "Bob", "Carlos" };

ICoffeeMachine machine = new CachingProxy(
new ProtectionProxy(
new VirtualProxy(),
"Alice",
authorizedBaristas));

Console.WriteLine($"Máy sẵn sàng? {machine.IsReady()}");
// Máy sẵn sàng? False (chưa tạo — lazy!)

Console.WriteLine("\n--- Đơn đầu tiên ---");
var drink1 = machine.Brew("Latte", CupSize.Large);
// [RealCoffeeMachine] Đang làm nóng nồi hơi... ← tạo lần đầu sử dụng
// [RealCoffeeMachine] Đang hiệu chuẩn áp suất...
// [RealCoffeeMachine] Máy sẵn sàng!
// [ProtectionProxy] Cấp quyền cho Alice
// [CachingProxy] Cache miss cho Latte:Large
// [RealCoffeeMachine] Đang pha Large Latte...

Console.WriteLine("\n--- Cùng đơn lần nữa ---");
var drink2 = machine.Brew("Latte", CupSize.Large);
// [CachingProxy] Cache hit cho Latte:Large ← không cần pha lại!

Console.WriteLine("\n--- Người dùng không có quyền ---");
ICoffeeMachine unauthorizedMachine = new ProtectionProxy(
new VirtualProxy(),
"Eve",
authorizedBaristas);

try { unauthorizedMachine.Brew("Espresso", CupSize.Small); }
catch (UnauthorizedAccessException ex) { Console.WriteLine($" BỊ CHẶN: {ex.Message}"); }
// BỊ CHẶN: Eve không có quyền sử dụng máy này.
mẹo

Chú ý việc xếp chồng (Stacking): CachingProxy → ProtectionProxy → VirtualProxy → RealCoffeeMachine. Mỗi proxy thêm một mối quan tâm (Concern). Client thấy ICoffeeMachine và không biết (hoặc không quan tâm) có bao nhiêu proxy trong chuỗi.

Các loại Proxy (Proxy Types)

LoạiMục đíchVí dụ
VirtualKhởi tạo lười biếng (Lazy Initialization)VirtualProxy tạo máy lần đầu sử dụng
ProtectionKiểm soát truy cập (Access Control)ProtectionProxy kiểm tra quyền người dùng
CachingLưu trữ và tái sử dụng kết quảCachingProxy tránh pha lại đơn giống hệt
RemoteẨn giao tiếp mạng (Network Communication)gRPC client stub, WCF proxy
LoggingGhi lại thao tác (Record Operations)Ghi mọi lệnh Brew() để audit
SmartĐếm tham chiếu (Reference Counting), lockingTheo dõi usage, tự giải phóng tài nguyên

Proxy vs Decorator

ProxyDecorator
Mục đíchKiểm soát truy cậpThêm hành vi
Tạo lậpProxy có thể tạo object thậtClient cung cấp object bị decorate
Vòng đời (Lifecycle)Proxy quản lý vòng đời của object thậtDecorator không quản lý vòng đời
Minh bạchClient không nên biết đó là proxyClient cố ý xếp chồng decorator

Sử dụng thực tế trong .NET (.NET Real-World Usage)

  • Lazy<T> — virtual proxy tích hợp sẵn, tạo giá trị khi truy cập lần đầu
  • EF Core lazy loading proxyILazyLoader tạo proxy class load navigation property khi truy cập
  • HttpClient — proxy giao tiếp HTTP đến server từ xa
  • WCF / gRPC client stub — remote proxy ẩn chi tiết mạng
  • ASP.NET Core RemoteAuthenticationService — proxy bao thao tác auth từ xa

Khi nào sử dụng (When to Use)

  • Cần kiểm soát truy cập đến object (bảo mật, quyền)
  • Object thật đắt để tạo và nên load lười biếng (Lazy Load)
  • Muốn thêm caching, logging, hoặc monitoring minh bạch
  • Object thật nằm trên máy từ xa và cần đại diện (Representative) cục bộ

Khi nào KHÔNG sử dụng (When NOT to Use)

  • Truy cập trực tiếp object là đủ — không cần kiểm soát truy cập hay lazy loading
  • Overhead của proxy không đáng (tối ưu hóa premature)
  • Proxy thêm nhiều complexity hơn giá trị

Điểm chính (Key Takeaways)

  • Proxy kiểm soát truy cập đến object khác thông qua cùng interface
  • Nhiều loại proxy phục vụ mục đích khác nhau: virtual, protection, caching, remote
  • Proxy có thể xếp chồng (Stacked) — mỗi cái xử lý một mối quan tâm (Concern)
  • Proxy có thể tạo object thật (virtual proxy) hoặc chỉ tham chiếu (protection, caching)

Câu hỏi phỏng vấn (Interview Questions)

Q: Proxy khác Decorator như thế nào? Về cấu trúc chúng gần như giống hệt nhau — cả hai đều bao object và implement cùng interface. Khác biệt là mục đích (Intent): Proxy kiểm soát truy cập (lazy loading, bảo mật), Decorator thêm hành vi (topping, middleware). Proxy thường quản lý vòng đời (Lifecycle) của object thật; Decorator thì không.

Q: EF Core sử dụng proxy như thế nào? EF Core có thể tạo dynamic proxy class lúc runtime kế thừa từ entity của bạn. Proxy này override virtual navigation property để lazy-load dữ liệu liên quan từ database khi truy cập lần đầu — classic virtual proxy.

Q: Remote proxy là gì? Remote proxy ẩn độ phức tạp của giao tiếp mạng (Network Communication). Client gọi method trên object cục bộ (proxy), proxy serialize lệnh gọi, gửi qua mạng, và trả về kết quả. gRPC và WCF client stub là remote proxy.

  • Decorator — cấu trúc giống, mục đích khác (thêm hành vi)
  • Adapter — thay đổi interface, Proxy giữ cùng interface
  • Facade — đơn giản hóa interface, Proxy không đơn giản hóa — nó kiểm soát
  • Lazy Loading trong EF Core — sử dụng proxy thực tế trong .NET