Proxy Pattern
1. Giới Thiệu
1.1 Định Nghĩa
Proxy Pattern là một structural pattern cung cấp một surrogate hoặc placeholder cho một đối tượng khác để kiểm soát truy cập đến đối tượng đó. Proxy đóng vai trò trung gian, cho phép thực hiện một số tác vụ trước hoặc sau khi truy cập đối tượng thực.
1.2 Mục Đích
- Kiểm soát truy cập đến đối tượng
- Thêm functionality trước/sau khi truy cập
- Lazy loading của đối tượng nặng
- Caching kết quả
- Logging và monitoring
2. Cấu Trúc Cơ Bản
2.1 Image Loading Example
// Subject interface
public interface Image {
void display();
}
// Real Subject
public class RealImage implements Image {
private String filename;
public RealImage(String filename) {
this.filename = filename;
loadFromDisk();
}
private void loadFromDisk() {
System.out.println("Loading " + filename);
}
@Override
public void display() {
System.out.println("Displaying " + filename);
}
}
// 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();
}
}
// Client
public class Client {
public static void main(String[] args) {
Image image = new ProxyImage("test.jpg");
// Image will be loaded from disk
image.display();
// Image will not be loaded from disk
image.display();
}
}
3. Ví Dụ Thực Tế
3.1 Database Connection Proxy
// Subject interface
public interface DatabaseConnection {
void connect();
void executeQuery(String query);
void disconnect();
}
// Real Subject
public class RealDatabaseConnection
implements DatabaseConnection {
private Connection connection;
@Override
public void connect() {
// Actual database connection
System.out.println("Connected to database");
}
@Override
public void executeQuery(String query) {
System.out.println("Executing query: " + query);
}
@Override
public void disconnect() {
System.out.println("Disconnected from database");
}
}
// Proxy
public class DatabaseConnectionProxy
implements DatabaseConnection {
private RealDatabaseConnection realConnection;
private final String username;
private final String password;
public DatabaseConnectionProxy(
String username, String password) {
this.username = username;
this.password = password;
}
@Override
public void connect() {
if (realConnection == null) {
if (authenticate()) {
realConnection =
new RealDatabaseConnection();
realConnection.connect();
} else {
throw new SecurityException(
"Authentication failed");
}
}
}
@Override
public void executeQuery(String query) {
if (realConnection == null) {
connect();
}
logQuery(query);
realConnection.executeQuery(query);
}
@Override
public void disconnect() {
if (realConnection != null) {
realConnection.disconnect();
}
}
private boolean authenticate() {
// Authentication logic
return true;
}
private void logQuery(String query) {
System.out.println("Logging query: " + query);
}
}
3.2 Caching Proxy
// Subject interface
public interface ExpensiveObject {
List<String> process(String input);
}
// Real Subject
public class RealExpensiveObject
implements ExpensiveObject {
@Override
public List<String> process(String input) {
// Expensive processing
try {
Thread.sleep(2000); // Simulate work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return Arrays.asList(
input.split(" "));
}
}
// Proxy
public class CachingProxy implements ExpensiveObject {
private RealExpensiveObject realObject;
private Map<String, List<String>> cache;
public CachingProxy() {
this.realObject = new RealExpensiveObject();
this.cache = new ConcurrentHashMap<>();
}
@Override
public List<String> process(String input) {
return cache.computeIfAbsent(
input, k -> realObject.process(k));
}
public void clearCache() {
cache.clear();
}
public int getCacheSize() {
return cache.size();
}
}
4. Spring Framework Integration
4.1 AOP Proxy Example
// Aspect
@Aspect
@Component
public class LoggingAspect {
private static final Logger logger =
LoggerFactory.getLogger(LoggingAspect.class);
@Around("@annotation(Loggable)")
public Object logMethod(
ProceedingJoinPoint joinPoint)
throws Throwable {
String methodName = joinPoint.getSignature()
.getName();
logger.info("Starting {}", methodName);
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long executionTime = System.currentTimeMillis() -
start;
logger.info("{} completed in {}ms",
methodName, executionTime);
return result;
}
}
// Annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
}
// Service
@Service
public class UserService {
@Loggable
public User findById(Long id) {
// Business logic
return new User(id);
}
}
4.2 Remote Proxy
// Remote interface
public interface RemoteService {
String getData(String key);
}
// Remote implementation
@Service
public class RemoteServiceImpl implements RemoteService {
@Override
public String getData(String key) {
// Actual remote call
return "Data for " + key;
}
}
// Proxy
@Service
public class RemoteServiceProxy implements RemoteService {
private final RemoteService remoteService;
private final Cache<String, String> cache;
public RemoteServiceProxy(RemoteService remoteService) {
this.remoteService = remoteService;
this.cache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(10))
.maximumSize(100)
.build();
}
@Override
public String getData(String key) {
return cache.get(key, k -> {
try {
return remoteService.getData(k);
} catch (Exception e) {
throw new RuntimeException(
"Failed to get data", e);
}
});
}
}
5. Testing Proxy Pattern
5.1 Unit Testing
@ExtendWith(MockitoExtension.class)
class DatabaseConnectionProxyTest {
@Mock
private RealDatabaseConnection realConnection;
@InjectMocks
private DatabaseConnectionProxy proxy;
@Test
void whenValidCredentials_thenConnect() {
// When
proxy.connect();
// Then
verify(realConnection).connect();
}
@Test
void whenInvalidCredentials_thenThrowException() {
// Given
DatabaseConnectionProxy proxy =
new DatabaseConnectionProxy(
"invalid", "invalid");
// When/Then
assertThrows(SecurityException.class,
() -> proxy.connect());
}
}
5.2 Integration Testing
@SpringBootTest
class RemoteServiceProxyTest {
@Autowired
private RemoteServiceProxy proxy;
@Test
void whenCached_thenFasterResponse() {
// First call - cache miss
long start = System.currentTimeMillis();
String result1 = proxy.getData("test");
long firstCallTime = System.currentTimeMillis() -
start;
// Second call - cache hit
start = System.currentTimeMillis();
String result2 = proxy.getData("test");
long secondCallTime = System.currentTimeMillis() -
start;
assertEquals(result1, result2);
assertTrue(secondCallTime < firstCallTime);
}
}
6. Lợi Ích và Nhược Điểm
6.1 Lợi Ích
- Kiểm soát truy cập tốt hơn
- Lazy loading và caching
- Logging và monitoring dễ dàng
- Tách biệt logic xử lý
- Bảo mật và validation
6.2 Nhược Điểm
- Tăng độ phức tạp của code
- Có thể ảnh hưởng đến hiệu suất
- Khó debug khi có nhiều proxy
- Có thể vi phạm Single Responsibility Principle
7. Best Practices
7.1 Virtual Proxy
public class VirtualProxyImage implements Image {
private volatile RealImage realImage;
private final String filename;
private final Object lock = new Object();
public VirtualProxyImage(String filename) {
this.filename = filename;
}
@Override
public void display() {
RealImage localRef = realImage;
if (localRef == null) {
synchronized (lock) {
localRef = realImage;
if (localRef == null) {
realImage = localRef =
new RealImage(filename);
}
}
}
localRef.display();
}
}
7.2 Protection Proxy
public class SecureDocumentProxy implements Document {
private RealDocument document;
private User user;
public SecureDocumentProxy(
RealDocument document, User user) {
this.document = document;
this.user = user;
}
@Override
public void view() {
if (hasViewPermission()) {
document.view();
} else {
throw new SecurityException(
"No view permission");
}
}
@Override
public void edit() {
if (hasEditPermission()) {
document.edit();
} else {
throw new SecurityException(
"No edit permission");
}
}
private boolean hasViewPermission() {
return user.hasPermission("VIEW");
}
private boolean hasEditPermission() {
return user.hasPermission("EDIT");
}
}
8. Common Issues và Solutions
8.1 Performance Optimization
public class SmartProxy implements Service {
private RealService service;
private LoadBalancer loadBalancer;
private CircuitBreaker circuitBreaker;
@Override
public Response execute(Request request) {
if (!circuitBreaker.isOpen()) {
try {
Server server = loadBalancer
.getNextServer();
return executeWithRetry(
request, server);
} catch (Exception e) {
circuitBreaker.recordFailure();
throw e;
}
}
throw new CircuitBreakerException();
}
private Response executeWithRetry(
Request request, Server server) {
int retries = 3;
Exception lastException = null;
while (retries > 0) {
try {
return service.execute(request);
} catch (Exception e) {
lastException = e;
retries--;
if (retries > 0) {
Thread.sleep(1000);
}
}
}
throw new RetryExhaustedException(
lastException);
}
}
8.2 Memory Management
public class MemoryAwareProxy implements Resource {
private WeakReference<RealResource> resourceRef;
private final ResourceFactory factory;
public MemoryAwareProxy(ResourceFactory factory) {
this.factory = factory;
}
@Override
public void use() {
RealResource resource = null;
if (resourceRef != null) {
resource = resourceRef.get();
}
if (resource == null) {
resource = factory.createResource();
resourceRef = new WeakReference<>(resource);
}
resource.use();
}
}
9. References
9.1 Books
- Design Patterns: Elements of Reusable Object-Oriented Software
- Head First Design Patterns
- Clean Architecture by Robert C. Martin
- Patterns of Enterprise Application Architecture