Adapter pattern: tích hợp cổng thanh toán cũ mà không kéo API của nó vào checkout Cơ bản
Thời gian đọc ước tính: 7 phút
Vấn đề
Phần tiêu đề “Vấn đề”Checkout muốn gọi charge({ orderId, amountMinor }) và nhận paymentId cùng trạng thái. Nhưng thư viện thanh toán cũ nhận pay(invoice, amountInCents) và trả { transaction, ok }.
Nếu checkout gọi thẳng thư viện, tên invoice, cờ ok và mọi thay đổi riêng của nhà cung cấp len vào logic xử lý đơn. Đổi gateway nghĩa là sửa cả luồng checkout. Adapter đặt một lớp dịch nhỏ đúng ở ranh giới đó.
Before và after
Phần tiêu đề “Before và after”| Before: gọi thẳng SDK | After: port + adapter | |
|---|---|---|
| Checkout import | LegacyGateway |
PaymentPort (thuộc phía ứng dụng) |
| Tên dùng trong use case | invoice, transaction, ok |
orderId, paymentId, status |
| Đổi gateway | Sửa checkout | Viết adapter mới, checkout giữ nguyên |
| Test checkout | Phải mock SDK | Dùng fake PaymentPort |
| Nơi dịch lỗi và đơn vị tiền | Rải rác khắp use case | Một chỗ, có thể review |
Đang tải diagram…
Khái niệm cốt lõi
Phần tiêu đề “Khái niệm cốt lõi”Adapter có ba vai:
- Client: checkout.
- Target: interface client mong muốn, ở đây
PaymentPort.charge. - Adaptee: API đã có nhưng không khớp, ở đây
LegacyGateway.pay.
Adapter implement PaymentPort, giữ một LegacyGateway, gọi nó rồi đổi kết quả về kiểu checkout hiểu. Đây là object adapter (composition, không kế thừa).
Điểm đáng nhớ là chiều phụ thuộc: checkout phụ thuộc PaymentPort thuộc phía ứng dụng, còn gateway cụ thể chỉ xuất hiện trong adapter. PaymentPort phải mô tả hành động ứng dụng cần, không chép chữ ký của SDK; nếu không, ngôn ngữ của gateway vẫn thống trị code bên trong.
Liên hệ với hexagonal và ACL
Phần tiêu đề “Liên hệ với hexagonal và ACL”- Hexagonal (ports and adapters):
PaymentPortlà một port (driven/outbound),LegacyGatewayAdapterlà adapter của nó. Adapter pattern là cơ chế cấp class của ý tưởng kiến trúc này. - Anti-corruption layer (ACL): khi phía bên kia có model khác hẳn (legacy, hệ thống bên ngoài), adapter đóng vai ACL: giữ model nội bộ sạch. Theo Microsoft Learn, ACL chỉ nên làm phần dịch, không chứa business rule hay orchestration. Xem thêm context map.
Cách hoạt động
Phần tiêu đề “Cách hoạt động”- Checkout kiểm tra
amountMinorlà số nguyên dương an toàn, rồi gọiPaymentPort.charge. - Adapter gọi
legacy.pay(input.orderId, input.amountMinor), nhận{ transaction, ok }, tạoChargeResult. - Checkout chỉ đọc
paymentIdvàstatustheo contract nội bộ.
Adapter được tạo ở composition root (bootstrap hoặc module wiring), không phải trong hàm checkout. Nhờ đó test thay được bằng fake và thêm provider không phải mở use case.
Adapter dịch được tên field, kiểu, trạng thái, lỗi, nhưng phải giữ ngữ nghĩa. ok: true có thể chỉ là “đã nhận yêu cầu”, không phải “tiền đã thu”.
Đọc tài liệu provider để biết:
- Đơn vị tiền nhỏ nhất.
- Xử lý đồng bộ hay bất đồng bộ.
- Mã lỗi.
- Idempotency.
- Lúc nào giao dịch là cuối cùng.
Nếu provider chỉ trả token để tra sau, ChargeResult phải có trạng thái pending, không ép vào accepted | declined.
Phân loại lỗi trong adapter
Phần tiêu đề “Phân loại lỗi trong adapter”Không bắt mọi exception rồi trả declined. Adapter là nơi phân loại tín hiệu từ provider thành các nhóm checkout xử lý khác nhau:
| Tín hiệu từ provider | Phân loại | Checkout nên làm | Retry? |
|---|---|---|---|
| Thẻ bị từ chối, không đủ tiền | Declined (business) | Báo người dùng, không retry | Không |
| Request sai định dạng, auth sai | Permanent error (lỗi của ta) | Alert, sửa code/config | Không |
| Provider báo lỗi tạm thời khi chưa xử lý (ví dụ 503 theo contract) | Retryable | Retry có backoff và jitter | Có, với idempotency key |
| Timeout, mất kết nối sau khi gửi | Unknown outcome | Đặt trạng thái pending, tra cứu/reconcile trước khi làm gì tiếp | Chỉ với idempotency key |
Cột “Phân loại” phải theo contract thật của từng provider; bảng chỉ là khung. Hành vi retry đúng cần backoff và jitter, deadline tổng và idempotency key. Adapter chỉ phân loại; workflow thanh toán lớn hơn mới quyết định pending, reconciliation và retry.
Kết quả port nên trung thực với các trạng thái có thể xảy ra, ví dụ (minh họa, không nằm trong file demo):
type ChargeResult = | { status: 'accepted'; paymentId: string } | { status: 'declined'; reason: string } | { status: 'pending'; paymentId?: string }; // unknown outcome: cần reconcileNếu hai provider khác đơn vị tiền, adapter phải đổi có quy tắc và test rõ, không âm thầm nhân chia 100. Currency là thuộc tính nghiệp vụ nên nằm trong port, không hard-code trong adapter.
Ví dụ thực tế
Phần tiêu đề “Ví dụ thực tế”Chạy demo
Phần tiêu đề “Chạy demo”npx --prefix examples ts-node examples/src/adapter-pattern/payment-adapter-demo.tsOutput thật:
{ paymentId: 'tx-order-42-12500', status: 'accepted' }LegacyGateway trong demo là class giả lập, không gọi mạng. Đổi transaction thành transactionId ở gateway: TypeScript chỉ yêu cầu sửa adapter. Đó là kiểm chứng nhỏ về vị trí của coupling.
Production cần ba lớp test với mục đích khác nhau:
- Unit test adapter với fake gateway: mapping field và phân loại lỗi.
- Contract/integration test với sandbox của provider: phát hiện API thật khác giả định.
- Test use case checkout với fake
PaymentPort: xử lýaccepted,declined,pending.
Adapter, Strategy, ACL: khác nhau thế nào
Phần tiêu đề “Adapter, Strategy, ACL: khác nhau thế nào”| Adapter | Strategy | ACL | |
|---|---|---|---|
| Giải quyết | Interface không khớp | Chọn giữa nhiều thuật toán cùng contract | Bảo vệ model khỏi hệ thống ngoài |
| Cấp độ | Pattern ở cấp class | Pattern ở cấp class | Cấp kiến trúc, thường hiện thực bằng adapter/facade |
| Có thể dùng chung | Mỗi gateway một adapter | Chọn gateway theo merchant | Adapter là một cách làm ACL |
Khi nào dùng / không dùng
Phần tiêu đề “Khi nào dùng / không dùng”- Dùng khi có khác biệt cụ thể giữa interface ứng dụng muốn và interface bên ngoài, và chi phí đổi là thật (SDK có thể đổi, nhiều provider).
- Không dùng cho helper nội bộ ổn định chỉ vì hai hàm khác tên, hoặc khi chỉ có một gateway và contract đã khớp.
Trade-offs
Phần tiêu đề “Trade-offs”Giá trị
- Tên gọi và trạng thái riêng của provider ở một chỗ, không lan vào checkout.
- Dễ thay SDK/provider và test use case bằng fake port.
- Mapping là điểm review rõ cho tiền, lỗi và trạng thái.
Chi phí
- Thêm lớp và contract cần duy trì; nhiều adapter mỏng cho code nội bộ gây nhiễu.
- Contract chung có thể che khác biệt quan trọng giữa provider nếu quá đơn giản.
- Không tự giải quyết retry, idempotency, timeout hay giao dịch bất định.
Sai lầm thường gặp
Phần tiêu đề “Sai lầm thường gặp”Checklist review một adapter
Phần tiêu đề “Checklist review một adapter”- Port dùng ngôn ngữ của ứng dụng (
orderId,paymentId,status), không chép chữ ký SDK. - Use case chỉ import port; SDK chỉ xuất hiện trong adapter, và adapter được tạo ở composition root.
- Mapping giữ ngữ nghĩa: trạng thái chưa chắc chắn có
pendingriêng, không ép vàoaccepted | declined. - Lỗi được phân loại thành declined, permanent, retryable, unknown outcome theo contract thật của provider; không bắt mọi exception rồi trả
declined. - Đổi đơn vị tiền có quy tắc và test rõ; currency nằm trong port, không hard-code trong adapter.
- Có đủ ba lớp test: unit test adapter với fake gateway, contract test với sandbox, test use case với fake port.
Tóm tắt
Phần tiêu đề “Tóm tắt”Tự kiểm tra:
- SDK đổi tên
transactionthì file nào cần sửa? - Vì sao timeout không thể luôn map thành
declined? - Khi nào một adapter mỏng chỉ làm code khó đọc hơn?