Vấn đề
Ở bài trước về Singleton, Factory, Builder, chúng ta đã đi qua nhóm Creational — các pattern kiểm soát việc tạo object. Bài này tiếp tục với ba pattern thường xuyên xuất hiện trong hệ thống thực tế: Strategy và Observer thuộc nhóm Behavioral, Decorator thuộc nhóm Structural.
Strategy
Strategy cho phép định nghĩa một tập hợp các thuật toán, đóng gói từng cái vào một class riêng, và hoán đổi chúng linh hoạt lúc runtime — mà không cần thay đổi code của class sử dụng chúng.
Vấn đề điển hình: một hệ thống thanh toán cần hỗ trợ nhiều phương thức — credit card, PayPal, crypto. Nếu viết thẳng vào một class:
public class PaymentService {
public void pay(String method, double amount) {
if (method.equals("CREDIT_CARD")) {
// logic credit card
} else if (method.equals("PAYPAL")) {
// logic paypal
} else if (method.equals("CRYPTO")) {
// logic crypto
}
}
}
Mỗi lần thêm phương thức mới là phải sửa PaymentService — vi phạm Open/Closed Principle. Logic các phương thức thanh toán cũng bị nhồi vào một class, khó test riêng từng cái.
Strategy tách mỗi thuật toán ra thành một class riêng:
public interface PaymentStrategy {
void pay(double amount);
}
public class CreditCardPayment implements PaymentStrategy {
private final String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(double amount) {
System.out.printf("Thanh toán %.2f qua Credit Card %s%n", amount, cardNumber);
}
}
public class PayPalPayment implements PaymentStrategy {
private final String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(double amount) {
System.out.printf("Thanh toán %.2f qua PayPal (%s)%n", amount, email);
}
}
public class CryptoPayment implements PaymentStrategy {
private final String walletAddress;
public CryptoPayment(String walletAddress) {
this.walletAddress = walletAddress;
}
@Override
public void pay(double amount) {
System.out.printf("Thanh toán %.2f qua Crypto (%s)%n", amount, walletAddress);
}
}
public class PaymentService {
private PaymentStrategy strategy;
public PaymentService(PaymentStrategy strategy) {
this.strategy = strategy;
}
// Có thể đổi strategy lúc runtime
public void setStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void checkout(double amount) {
strategy.pay(amount);
}
}
PaymentService service = new PaymentService(new CreditCardPayment("4111-1111-1111-1111"));
service.checkout(500_000);
// Đổi sang PayPal lúc runtime — không cần sửa PaymentService
service.setStrategy(new PayPalPayment("alice@example.com"));
service.checkout(200_000);
Thêm CryptoPayment hay bất kỳ phương thức nào khác chỉ cần tạo class mới implement PaymentStrategy — PaymentService không cần động đến.
Từ Java 8, Strategy có thể được implement bằng lambda khi interface chỉ có một method:
PaymentStrategy quickPay = amount ->
System.out.printf("Quick pay: %.2f%n", amount);
service.setStrategy(quickPay);
Dùng Strategy trong hệ thống lớn như thế nào?
Strategy xuất hiện rất nhiều trong các framework. Comparator trong Java là một Strategy — bạn truyền vào logic so sánh tùy ý mà không cần thay đổi Collections.sort(). AuthenticationProvider trong Spring Security cũng là Strategy — framework gọi từng provider theo thứ tự cho đến khi một cái xác thực thành công.
Trong hệ thống lớn, Strategy còn được dùng để implement A/B testing hay feature flag — swap thuật toán pricing, recommendation, hay ranking theo từng nhóm user mà không deploy lại code.
Những điểm dễ nhầm khi phỏng vấn
“Strategy và Factory khác nhau như thế nào?”
Factory quan tâm đến việc tạo object. Strategy quan tâm đến việc chọn behavior. Trong thực tế, hai pattern này thường đi cùng nhau: Factory tạo ra Strategy phù hợp dựa trên config hay input, rồi inject vào context.
“Strategy có khác gì Template Method không?”
Template Method dùng inheritance — class cha định nghĩa skeleton của thuật toán, subclass override từng bước. Strategy dùng composition — behavior được inject từ bên ngoài. Strategy linh hoạt hơn vì có thể đổi lúc runtime, còn Template Method bị gắn chặt lúc compile time.
Observer
Observer định nghĩa quan hệ một-nhiều giữa các object: khi một object thay đổi trạng thái, tất cả các object phụ thuộc vào nó được thông báo và tự động cập nhật.
Object được quan sát gọi là Subject (hay Publisher), các object quan sát gọi là Observer (hay Subscriber). Đây là nền tảng của event-driven architecture.
Lấy ví dụ hệ thống order: khi một order được đặt thành công, cần gửi email xác nhận, cập nhật inventory và ghi audit log — ba việc hoàn toàn độc lập nhau.
Nếu viết thẳng vào OrderService:
public class OrderService {
public void placeOrder(Order order) {
// Xử lý order
emailService.sendConfirmation(order);
inventoryService.updateStock(order);
auditService.log(order);
}
}
OrderService đang phụ thuộc trực tiếp vào ba service khác — tightly coupled. Thêm một action mới (ví dụ push notification) là phải sửa OrderService.
Observer giải quyết bằng cách để OrderService chỉ phát ra event, không quan tâm ai xử lý:
public interface OrderObserver {
void onOrderPlaced(Order order);
}
public class EmailNotificationObserver implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
System.out.println("Gửi email xác nhận cho: " + order.getCustomerEmail());
}
}
public class InventoryObserver implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
System.out.println("Cập nhật tồn kho cho đơn: " + order.getId());
}
}
public class AuditObserver implements OrderObserver {
@Override
public void onOrderPlaced(Order order) {
System.out.println("Ghi audit log cho đơn: " + order.getId());
}
}
public class OrderService {
private final List<OrderObserver> observers = new ArrayList<>();
public void addObserver(OrderObserver observer) {
observers.add(observer);
}
public void removeObserver(OrderObserver observer) {
observers.remove(observer);
}
public void placeOrder(Order order) {
// Xử lý order
System.out.println("Đặt hàng thành công: " + order.getId());
notifyObservers(order);
}
private void notifyObservers(Order order) {
observers.forEach(observer -> observer.onOrderPlaced(order));
}
}
OrderService orderService = new OrderService();
orderService.addObserver(new EmailNotificationObserver());
orderService.addObserver(new InventoryObserver());
orderService.addObserver(new AuditObserver());
orderService.placeOrder(new Order("ORD-001", "alice@example.com"));
// Đặt hàng thành công: ORD-001
// Gửi email xác nhận cho: alice@example.com
// Cập nhật tồn kho cho đơn: ORD-001
// Ghi audit log cho đơn: ORD-001
Thêm push notification chỉ cần tạo PushNotificationObserver và addObserver — OrderService không cần sửa.
Dùng Observer trong hệ thống lớn như thế nào?
Observer là nền tảng của hầu hết các event system. Spring ApplicationEventPublisher implement Observer pattern — bạn publish event, các @EventListener tự nhận và xử lý. RxJava, Project Reactor (dùng trong Spring WebFlux) cũng xây dựng trên Observer pattern nhưng mở rộng thêm với backpressure và async processing.
Trong distributed system, Observer pattern được scale lên thành message queue — Kafka, RabbitMQ. Subject không gọi trực tiếp observer mà publish message lên queue, các consumer tự subscribe và xử lý độc lập.
Những điểm dễ nhầm khi phỏng vấn
“Observer và Event Bus khác nhau như thế nào?”
Observer truyền thống là synchronous và tightly coupled về mặt vật lý — Subject giữ reference trực tiếp đến Observer. Event Bus (Guava EventBus, Spring ApplicationEventPublisher) thêm một tầng trung gian — Subject và Observer không biết nhau, chỉ biết event type. Event Bus cũng thường hỗ trợ async dễ hơn.
“Memory leak trong Observer xảy ra khi nào?”
Khi Subject giữ reference đến Observer mà Observer không được remove trước khi bị garbage collect. Nếu Observer là một UI component (ví dụ Android Activity) bị destroy nhưng Subject vẫn còn sống, Subject vẫn giữ reference đến Activity đó — memory leak. Luôn nhớ removeObserver() khi Observer không còn cần thiết.
Decorator
Decorator cho phép thêm behavior vào object lúc runtime bằng cách wrap nó trong một object khác — thay vì dùng inheritance để mở rộng.
Lấy ví dụ hệ thống notification đã quen. Giả sử EmailNotification cần được mở rộng: đôi khi cần log, đôi khi cần compress message, đôi khi cần cả hai. Nếu dùng inheritance:
EmailNotification
├── LoggingEmailNotification
├── CompressingEmailNotification
└── LoggingCompressingEmailNotification // Class bùng nổ
Với mỗi combination mới lại cần thêm một subclass. Decorator giải quyết bằng cách wrap object:
public interface Notification {
void send(String message);
}
public class EmailNotification implements Notification {
private final String recipient;
public EmailNotification(String recipient) {
this.recipient = recipient;
}
@Override
public void send(String message) {
System.out.printf("Email đến %s: %s%n", recipient, message);
}
}
// Base decorator — wrap một Notification khác
public abstract class NotificationDecorator implements Notification {
protected final Notification wrapped;
public NotificationDecorator(Notification wrapped) {
this.wrapped = wrapped;
}
@Override
public void send(String message) {
wrapped.send(message);
}
}
public class LoggingDecorator extends NotificationDecorator {
public LoggingDecorator(Notification wrapped) {
super(wrapped);
}
@Override
public void send(String message) {
System.out.println("[LOG] Chuẩn bị gửi: " + message);
wrapped.send(message);
System.out.println("[LOG] Đã gửi xong");
}
}
public class CompressionDecorator extends NotificationDecorator {
public CompressionDecorator(Notification wrapped) {
super(wrapped);
}
@Override
public void send(String message) {
String compressed = compress(message);
wrapped.send(compressed);
}
private String compress(String message) {
// Giả lập compress
return "[compressed] " + message;
}
}
public class RetryDecorator extends NotificationDecorator {
private final int maxRetries;
public RetryDecorator(Notification wrapped, int maxRetries) {
super(wrapped);
this.maxRetries = maxRetries;
}
@Override
public void send(String message) {
for (int i = 0; i < maxRetries; i++) {
try {
wrapped.send(message);
return;
} catch (Exception e) {
System.out.printf("Lần thử %d thất bại, thử lại...%n", i + 1);
}
}
throw new RuntimeException("Gửi thất bại sau " + maxRetries + " lần thử");
}
}
Bây giờ có thể compose behavior tùy ý lúc runtime:
// Chỉ log
Notification withLog = new LoggingDecorator(
new EmailNotification("alice@example.com")
);
// Log + compress
Notification withLogAndCompress = new LoggingDecorator(
new CompressionDecorator(
new EmailNotification("alice@example.com")
)
);
// Log + compress + retry
Notification full = new LoggingDecorator(
new CompressionDecorator(
new RetryDecorator(
new EmailNotification("alice@example.com"), 3
)
)
);
full.send("Đơn hàng của bạn đã được xác nhận");
Không cần thêm bất kỳ subclass mới nào — chỉ compose các decorator theo nhu cầu.
Dùng Decorator trong hệ thống lớn như thế nào?
Decorator có mặt ở khắp nơi trong Java ecosystem. BufferedReader, InputStreamReader, FileInputStream trong Java I/O đều là decorator — bạn wrap stream vào stream để thêm buffering, encoding, hay compression. Spring Security cũng dùng decorator để wrap HttpServletRequest và thêm các security feature theo từng layer.
Trong hệ thống lớn, Decorator thường được dùng để implement cross-cutting concerns như logging, caching, rate limiting, authentication — những thứ không thuộc về business logic nhưng cần áp dụng lên nhiều nơi. Thay vì nhét vào từng service, bạn wrap service trong decorator và inject vào. Đây cũng là cách AOP (Aspect-Oriented Programming) hoạt động về mặt khái niệm.
Những điểm dễ nhầm khi phỏng vấn
“Decorator và Inheritance khác nhau như thế nào?”
Inheritance mở rộng behavior lúc compile time — bạn phải biết trước cần combination nào. Decorator mở rộng lúc runtime — bạn compose tùy ý theo nhu cầu. Inheritance tạo ra class hierarchy cứng nhắc, Decorator tạo ra object graph linh hoạt. Rule of thumb: nếu số lượng combination có thể bùng nổ, Decorator là lựa chọn tốt hơn.
“Decorator và Proxy khác nhau như thế nào?”
Cả hai đều wrap object, nhưng mục đích khác nhau. Decorator thêm behavior mới — làm phong phú thêm object. Proxy kiểm soát access đến object — lazy loading, access control, remote proxy. Proxy thường biết rõ class cụ thể của object nó wrap, Decorator làm việc thông qua interface và không quan tâm implementation bên trong.
Tổng kết
Ba pattern này giải quyết các bài toán khác nhau nhưng có một điểm chung: tất cả đều ưu tiên composition over inheritance và coding to interface.
- Strategy — đóng gói thuật toán thay thế được, dùng khi cần hoán đổi behavior lúc runtime mà không thay đổi context
- Observer — decoupling subject khỏi các observer, dùng khi một sự kiện cần trigger nhiều action độc lập nhau
- Decorator — thêm behavior vào object bằng cách wrap, dùng khi cần kết hợp nhiều behavior linh hoạt mà không bùng nổ subclass
Kết hợp với Singleton, Factory, Builder, sáu pattern này bao phủ phần lớn các tình huống thiết kế thường gặp trong hệ thống Java thực tế.
