Facade pattern đặt một lớp đơn giản đứng trước một nhóm lớp phức tạp, để client chỉ cần gọi một phương thức thay vì điều phối năm sáu dịch vụ theo đúng thứ tự. Trong một hệ thống bán hàng, "đặt đơn" thường là giữ tồn kho, tính giá, thanh toán, tạo vận đơn rồi gửi thông báo, kèm logic hoàn tác khi một bước hỏng giữa chừng. Bài này trình bày Facade pattern trong Java qua một ví dụ đặt hàng chạy thật với mã tương thích Java 17. Nội dung gồm cấu trúc, cách xử lý lỗi và hoàn tác, cách phân biệt với Adapter, Proxy, Mediator. Bài cũng nói về những lúc Facade biến thành "god object" cần tránh.

Trong series, các mẫu gần với Facade nhất là những mẫu bọc đối tượng: Adapter pattern: nối API cũ với giao diện mới, Decorator pattern và Proxy pattern: JDK dynamic proxy, CGLIB và Spring AOP. Facade pattern cũng "đứng trước" đối tượng khác, nhưng mục tiêu là giản lược cả một hệ thống con, không phải một đối tượng đơn lẻ. Nền tảng thiết kế liên quan có trong Nguyên lý SOLID trong Java qua ví dụ thực tế, đặc biệt nguyên lý Interface Segregation.

Vấn đề: client phải biết quá nhiều

Giả sử bạn có năm dịch vụ: InventoryService (kho), PricingService (giá và phí giao), PaymentService, ShippingService và NotificationService. Controller web, job nhập đơn từ sàn thương mại điện tử và công cụ nội bộ của nhân viên đều cần "đặt đơn". Nếu mỗi nơi tự điều phối năm dịch vụ, bạn sẽ gặp ba vấn đề.

  • Lặp logic. Thứ tự gọi, cách tính tổng tiền và quy tắc hoàn tác bị sao chép ở ba nơi. Sửa một quy tắc phải sửa ba chỗ.
  • Coupling cao. Cả ba nơi phụ thuộc vào năm lớp cụ thể. Đổi nhà cung cấp vận chuyển kéo theo sửa cả ba.
  • Dễ sai. Quên hoàn kho khi thanh toán thất bại là lỗi nghiệp vụ nghiêm trọng, nhưng không có gì buộc người viết client nhớ điều đó.

Facade pattern gom phần điều phối vào một chỗ duy nhất. Client chỉ biết placeOrder(request).

Cấu trúc của Facade pattern

Theo mô tả của refactoring.guru, Facade pattern "provides a simplified interface to a library, a framework, or any other complex set of classes". Các thành phần:

Vai tròNhiệm vụTrong ví dụ đặt hàng
FacadeCung cấp truy cập thuận tiện tới một phần hệ thống con, biết chuyển yêu cầu cho thành phần nàoOrderFacade
Additional FacadeFacade phụ, giữ cho một facade không phình quá rộngCó thể tách RefundFacade
SubsystemNhiều đối tượng phụ thuộc lẫn nhau, không biết facade tồn tạiKho, giá, thanh toán, vận chuyển, thông báo
ClientGọi facade thay vì gọi trực tiếp hệ thống conController, job nhập đơn

Hai đặc điểm quan trọng của Facade pattern. Thứ nhất, hệ thống con không biết facade tồn tại, nên bạn thêm Facade pattern vào hệ thống đang chạy mà không phải sửa các lớp cũ. Thứ hai, facade không cấm truy cập trực tiếp: client nào cần tính năng chuyên sâu vẫn gọi thẳng hệ thống con. Facade pattern là lối tắt tiện lợi, không phải bức tường.

Cài đặt Facade pattern: một cửa vào cho đặt hàng

Các dịch vụ con trong ví dụ là các lớp nhỏ chạy trong bộ nhớ, đủ để thấy luồng điều phối. Kho có thao tác giữ và trả hàng, thanh toán có thể từ chối khi vượt hạn mức, vận chuyển có thể không có tuyến. Các đoạn code trong bài là bản rút gọn của code/s6-09/src/FacadeDemo.java (lược các dòng System.out.println, phần thân của release, refund và phần khai báo bốn dịch vụ còn lại); mã đầy đủ, đã chạy để lấy output bên dưới, nằm trong file đó:

static class InventoryService {
    private final Map<String, Integer> stock = new HashMap<>(Map.of("SP-001", 5, "SP-002", 0));
    boolean reserve(String sku, int qty) {
        int cur = stock.getOrDefault(sku, 0);
        if (cur < qty) return false;
        stock.put(sku, cur - qty);
        System.out.println("   [kho] giữ " + qty + " x " + sku + ", còn " + stock.get(sku));
        return true;
    }
    void release(String sku, int qty) { /* cộng lại tồn kho */ }
}

static class PaymentService {
    private final long limit;
    PaymentService(long limit) { this.limit = limit; }
    String charge(String customer, long amount) {
        if (amount > limit) throw new IllegalStateException("thẻ bị từ chối: " + amount + " > hạn mức " + limit);
        return "TXN-" + customer + "-" + amount;
    }
    void refund(String txn) { /* hoàn tiền */ }
}

Facade pattern ở đây nhận năm dịch vụ qua constructor (không tự new, để dễ thay thế khi kiểm thử) và đóng gói toàn bộ luồng:

record OrderRequest(String customer, String sku, int qty, String city) {}
record OrderResult(boolean ok, String message, String txn, String shipment) {}

static class OrderFacade {
    // ... năm dịch vụ nhận qua constructor ...

    OrderResult placeOrder(OrderRequest r) {
        if (!inventory.reserve(r.sku(), r.qty())) {
            return new OrderResult(false, "hết hàng " + r.sku(), null, null);
        }
        try {
            long amount = pricing.total(r.sku(), r.qty()) + pricing.shippingFee(r.city());
            String txn = payment.charge(r.customer(), amount);
            try {
                String shipment = shipping.createShipment(r.city(), r.sku(), r.qty());
                notification.sms(r.customer(), "Đơn " + r.sku() + " x" + r.qty() + " đã xác nhận");
                return new OrderResult(true, "thành công", txn, shipment);
            } catch (RuntimeException e) {
                payment.refund(txn);
                throw e;
            }
        } catch (RuntimeException e) {
            inventory.release(r.sku(), r.qty());
            return new OrderResult(false, e.getMessage(), null, null);
        }
    }
}

Client không còn thấy kho, giá hay vận chuyển. Điều đáng giá nhất nằm ở các nhánh lỗi: facade là nơi duy nhất biết thứ tự hoàn tác. Mã chỉ dùng tính năng của Java 17 (record, Map.of); lệnh java --source 17 FacadeDemo.java chạy trên một bản JDK mới hơn và cho output thật bên dưới. Trường hợp thành công và hết hàng:

=== 1. Đặt hàng thành công (client chỉ gọi 1 phương thức) ===
   [kho] giữ 2 x SP-001, còn 3
   [thanh toán] trừ 260000 VND -> TXN-KH01-260000
   [vận chuyển] tạo vận đơn VD-462-SP-001
   [thông báo] gửi KH01: Đơn SP-001 x2 đã xác nhận
OrderResult[ok=true, message=thành công, txn=TXN-KH01-260000, shipment=VD-462-SP-001]
=== 2. Hết hàng ===
OrderResult[ok=false, message=hết hàng SP-002, txn=null, shipment=null]

Tổng tiền 260.000 VND gồm 2 sản phẩm giá 120.000 cộng phí giao 20.000 nội thành Hà Nội. Với đơn hết hàng, facade trả kết quả thất bại ngay mà không chạm tới thanh toán. Hai kịch bản lỗi còn lại cho thấy việc hoàn tác:

=== 3. Thanh toán bị từ chối -> Facade hoàn kho ===
Tồn SP-001 trước: 3
   [kho] giữ 3 x SP-001, còn 0
   [kho] trả lại 3 x SP-001, còn 3
OrderResult[ok=false, message=thẻ bị từ chối: 395000 > hạn mức 300000, txn=null, shipment=null]
Tồn SP-001 sau: 3
=== 3b. Vận chuyển lỗi -> Facade hoàn tiền rồi hoàn kho ===
   [kho] giữ 1 x SP-001, còn 2
   [thanh toán] trừ 155000 VND -> TXN-KH04-155000
   [thanh toán] hoàn TXN-KH04-155000
   [kho] trả lại 1 x SP-001, còn 3
OrderResult[ok=false, message=không có tuyến giao tới Đảo xa, txn=null, shipment=null]
Tồn SP-001 sau: 3

Ở kịch bản 3, tồn kho trước và sau đều là 3: facade đã trả lại số hàng đã giữ. Ở kịch bản 3b, lỗi xảy ra ở bước thứ ba nên facade hoàn tiền trước rồi mới hoàn kho, đúng thứ tự ngược với lúc thực hiện. Đây là phần mà client tự điều phối rất dễ quên.

Một lưu ý trung thực về Facade pattern: ví dụ này chạy trong một tiến trình, nên "hoàn tác bằng code" đủ dùng. Khi các dịch vụ là các hệ thống tách biệt qua mạng, việc hoàn tác có thể thất bại, tin nhắn có thể trùng, và bạn cần thêm các cơ chế như khoá idempotency và saga. Facade pattern vẫn là chỗ hợp lý để đặt điều phối, nhưng đừng coi đoạn catch ở trên là đủ cho môi trường phân tán.

Dùng Facade pattern thế nào cho đúng

Có vài quyết định thiết kế đáng cân nhắc khi cài Facade pattern:

  • Kiểu trả về. Ví dụ dùng OrderResult với cờ ok cho lỗi nghiệp vụ dự kiến (hết hàng, thẻ bị từ chối). Lỗi bất thường như mất kết nối nên là ngoại lệ. Tách bạch hai loại giúp client xử lý rõ ràng.
  • Interface hay lớp cụ thể. Nếu có nhiều client hoặc cần giả lập khi test, hãy cho facade cài một interface OrderPlacing. Client phụ thuộc interface, đúng tinh thần Dependency Inversion.
  • Không chứa luật nghiệp vụ phức tạp. Facade nên điều phối, không nên tính toán. Phần tính phí, khuyến mãi vẫn thuộc về PricingService.
  • Phạm vi theo ngữ cảnh. Một facade cho mỗi ngữ cảnh nghiệp vụ (đặt đơn, đổi trả, báo cáo), không phải một facade cho cả hệ thống.

Facade pattern trong JDK và thư viện phổ biến

Bạn đã dùng Facade pattern nhiều lần mà có thể không nhận ra:

  • SLF4J. Trang hướng dẫn chính thức mở đầu bằng câu "The Simple Logging Facade for Java (SLF4J) serves as a simple facade or abstraction for various logging frameworks". Mã ứng dụng gọi một API duy nhất, còn backend (Logback, reload4j, java.util.logging...) được chọn khi triển khai.
  • Lớp tiện ích như java.nio.file.Files. Một lời gọi Files.readString(path) che đi việc mở kênh, giải mã, đọc và đóng tài nguyên. Nó là lối tắt tiện lợi tới một hệ thống con lớn hơn, tương tự tinh thần Facade pattern, dù tài liệu JDK không gọi nó bằng tên mẫu này.
  • Spring JdbcTemplate và các lớp Template/Client tương tự. Chúng đóng gói việc lấy kết nối, tạo statement, xử lý ngoại lệ và đóng tài nguyên sau một vài phương thức. Nhiều tài liệu xếp chúng gần với Facade về mục đích đơn giản hoá, nhưng đồng thời chúng còn áp dụng Template Method, nên đây là ranh giới mờ.

Dấu hiệu chung: một API nhỏ phía trước, một hệ thống con lớn phía sau, và người dùng thông thường không cần nhìn vào bên trong.

Phân biệt Facade pattern với Adapter, Proxy và Mediator

Bốn mẫu này, kể cả Facade pattern, đều xen vào giữa hai bên nên dễ nhầm. Bảng sau tóm tắt theo refactoring.guru:

MẫuBọc bao nhiêu đối tượngGiao diệnMục đích
FacadeNhiều đối tượng (cả hệ thống con)Giao diện mới, đơn giản hơnGiản lược cách dùng hệ thống con
AdapterThường một đối tượngChuyển sang giao diện client mong đợiLàm hai giao diện không khớp dùng được với nhau
ProxyMột đối tượngGiữ nguyên giao diệnKiểm soát truy cập
MediatorNhiều đối tượngCác thành phần chỉ biết mediatorTập trung giao tiếp giữa các thành phần

Điểm khác giữa Facade pattern và Mediator rất đáng nhớ: Facade không thêm chức năng mới, và các đối tượng của hệ thống con vẫn có thể nói chuyện trực tiếp với nhau. Mediator thì bắt các thành phần giao tiếp qua nó và chúng không biết nhau. Với Proxy, vì dùng chung giao diện với đối tượng thật nên hai bên hoán đổi được; Facade đưa ra giao diện khác. Cách nhớ nhanh: Adapter đổi hình dạng, Proxy giữ hình dạng và canh cửa, Facade rút gọn nhiều cửa thành một.

Khi nào không nên dùng Facade pattern

Nhược điểm chính của Facade pattern được chính tài liệu của mẫu này nêu: facade có thể trở thành god object, gắn với mọi lớp trong ứng dụng. Các dấu hiệu bạn đang đi vào vùng nguy hiểm:

  • Facade có hàng chục phương thức không liên quan. Hãy tách thành nhiều facade theo ngữ cảnh nghiệp vụ (Additional Facade).
  • Facade pattern bị nhồi luật nghiệp vụ. Khi if tính khuyến mãi xuất hiện trong facade, phần đó nên thuộc về dịch vụ chuyên trách.
  • Mọi thay đổi đều chạm facade. Nếu thêm tính năng nào cũng sửa cùng một lớp, nó đang gánh quá nhiều trách nhiệm.
  • Hệ thống con vốn đã đơn giản. Bọc thêm một lớp cho hai lời gọi chỉ thêm lớp trung gian vô ích.
  • Facade che giấu lỗi cần xử lý ở client. Nếu mọi lỗi bị nuốt thành ok=false, client không phân biệt được lỗi người dùng với lỗi hệ thống.

Lỗi thường gặp khi dùng Facade pattern

  • Quên hoàn tác một bước trong Facade pattern. Mỗi bước thành công trước điểm lỗi cần có thao tác bù, theo thứ tự ngược. Viết test cho từng điểm lỗi, như hai kịch bản 3 và 3b ở trên.
  • Facade tự new hệ thống con. Khó thay thế khi kiểm thử. Nhận phụ thuộc qua constructor như ví dụ.
  • Cấm truy cập trực tiếp hệ thống con khi dùng Facade pattern. Biến facade thành điểm nghẽn thay vì lối tắt; hãy để tính năng chuyên sâu đi thẳng.
  • Dùng facade làm nơi chứa trạng thái. Facade nên không giữ trạng thái nghiệp vụ; trạng thái thuộc về hệ thống con hoặc kho dữ liệu.
  • Nhầm với Service layer. Trong ứng dụng Spring, lớp service thường đã làm việc điều phối. Đặt tên rõ OrderFacade chỉ khi nó thật sự che nhiều dịch vụ.

Checklist áp dụng Facade pattern

  1. Chỉ áp dụng Facade pattern khi có từ hai client trở lên cùng điều phối một chuỗi dịch vụ giống nhau.
  2. Facade chỉ điều phối và hoàn tác, không chứa luật nghiệp vụ.
  3. Phụ thuộc nhận qua constructor, client phụ thuộc interface nếu cần.
  4. Mỗi điểm lỗi có thao tác bù và có test riêng.
  5. Một Facade pattern cho mỗi ngữ cảnh nghiệp vụ, tách khi phình to.
  6. Hệ thống con vẫn truy cập trực tiếp được cho nhu cầu chuyên sâu.
  7. Nếu hệ thống con chạy qua mạng, bổ sung idempotency và cơ chế bù kiểu saga.

Bài tiếp theo

Facade pattern giản lược nhiều đối tượng thành một cửa vào; mẫu tiếp theo giải quyết một bài toán cấu trúc khác, là coi một nút đơn lẻ và cả một nhóm nút như nhau trong cùng cây. Để xem lại mẫu liền trước, hãy đọc Proxy pattern: JDK dynamic proxy, CGLIB và Spring AOP. Bài kế tiếp trong series: Composite pattern: xử lý cây đối tượng đồng nhất.