Backend

99 Ngày Java — Ngày 94: Strategy và Observer

SSite Admin
29 tháng 10, 2026 3 phút đọc 7 lượt xem
99 Ngày Java — Ngày 94: Strategy và Observer

Hai mẫu hành vi dùng nhiều nhất trong Java đều xoay quanh một ý: tách thứ thay đổi ra khỏi thứ ổn định. Strategy tách thuật toán khỏi nơi dùng nó. Observer tách nơi phát sinh sự kiện khỏi những nơi phản ứng với sự kiện. Từ Java 8, lambda làm cả hai mẫu gọn hơn rất nhiều.

Strategy và Observer

1. Strategy thay chuỗi if-else

Khi một phương thức rẽ nhánh theo loại để chọn cách tính, mỗi loại mới là một lần sửa phương thức đó và test lại tất cả. Strategy đưa mỗi cách tính vào một lớp cài đặt cùng interface. Nơi dùng chỉ giữ một tham chiếu tới interface và gọi nó. Việc chọn strategy nào thường nằm trong một Map tra theo khóa.

interface DiscountPolicy { Money apply(Money total); }

final class Checkout {
    private final Map<CustomerTier, DiscountPolicy> policies;
    Checkout(Map<CustomerTier, DiscountPolicy> policies) { this.policies = policies; }

    Money totalFor(Order order) {
        var policy = policies.getOrDefault(order.tier(), total -> total);
        return policy.apply(order.subtotal());
    }
}

2. Lambda là strategy

Một strategy chỉ có một phương thức chính là functional interface, nên lambda hoặc method reference thay được cả lớp cài đặt. Bạn đã dùng mẫu này mỗi ngày: Comparator truyền vào sort là một strategy so sánh. Lớp riêng vẫn đáng viết khi strategy có trạng thái, có phụ thuộc cần inject hoặc đủ phức tạp để cần test riêng.

Map<CustomerTier, DiscountPolicy> policies = Map.of(
        CustomerTier.GOLD, total -> total.multiply(0.90),
        CustomerTier.SILVER, total -> total.multiply(0.95));

orders.sort(Comparator.comparing(Order::createdAt).reversed()); // a strategy too

3. Observer: phát sự kiện, không gọi trực tiếp

Sau khi thanh toán xong cần gửi email, cộng điểm và ghi nhật ký. Nếu dịch vụ thanh toán gọi thẳng ba nơi đó, nó phải biết cả ba và mỗi việc mới lại sửa nó. Với Observer, nó chỉ phát một sự kiện; các listener tự đăng ký và tự phản ứng. Danh sách listener nên là CopyOnWriteArrayList để việc đăng ký trong lúc đang phát sự kiện không gây ConcurrentModificationException.

record OrderPaid(String orderId, Money amount) {}

final class OrderEvents {
    private final List<Consumer<OrderPaid>> listeners = new CopyOnWriteArrayList<>();

    Runnable subscribe(Consumer<OrderPaid> listener) {
        listeners.add(listener);
        return () -> listeners.remove(listener);   // call to unsubscribe
    }

    void publish(OrderPaid event) { listeners.forEach(l -> l.accept(event)); }
}

events.subscribe(event -> mailer.sendReceipt(event.orderId()));

4. Những điều cần cẩn thận

Listener được gọi đồng bộ theo mặc định: một listener chậm làm chậm bên phát, và một listener ném ngoại lệ có thể chặn các listener sau nếu bạn không bắt lỗi. Listener không hủy đăng ký sẽ bị bên phát giữ mãi, đúng kiểu memory leak đã nói ở ngày 80. Luồng xử lý cũng khó lần theo hơn vì không còn lời gọi trực tiếp nào để nhảy tới.

  • Sự kiện là record bất biến, đặt tên ở thì quá khứ: OrderPaid.

  • Bắt lỗi từng listener để một lỗi không chặn phần còn lại.

  • Việc bắt buộc phải thành công cùng giao dịch thì gọi trực tiếp, đừng giấu sau sự kiện.

  • java.util.Observable đã bị deprecated từ Java 9; tự viết hoặc dùng sự kiện của framework.

Strategy cho phép đổi cách làm, Observer cho phép thêm người phản ứng, cả hai mà không sửa code cũ. Ngày 95 kết thúc loạt pattern với hai mẫu cấu trúc: Decorator và Adapter.

Tài liệu đối chiếu

  • https://refactoring.guru/design-patterns/strategy

  • https://refactoring.guru/design-patterns/observer

S

Site Admin

Engineer and writer. Building things with TypeScript and distributed systems.

Bình luận (0)

Bạn cần đăng nhập bằng Google để bình luận.

Hãy là người bình luận đầu tiên.

Bài viết liên quan

99 Ngày Spring — Ngày 94: CI/CD cho Spring Boot

Workflow GitHub Actions để build, test và đóng image, đánh phiên bản bất biến và triển khai qua nhiều môi trường có kiểm soát.

29 thg 10, 20263 phút5
99 Ngày Spring — Ngày 93: Monitoring với Prometheus và Grafana

Phơi metric qua Actuator, Prometheus scrape, viết PromQL cho tỷ lệ lỗi và p99, dựng dashboard và alert cơ bản.

28 thg 10, 20263 phút8
99 Ngày Java — Ngày 93: Builder và Prototype

Builder cho đối tượng nhiều tham số, fluent API, kiểm tra trong build(), và sao chép đối tượng an toàn thay cho clone().

28 thg 10, 20263 phút6