TungDaDev's Blog

adapter pattern

Adapter pattern.webp
Published on
/9 mins read/

Trong cuốn sách mẫu kinh điển của Gang of Four (GoF), Adapter Pattern (còn được gọi là Wrapper) được định nghĩa rất ngắn gọn: "Chuyển đổi giao diện (interface) của một lớp thành một giao diện khác mà client mong đợi, cho phép các lớp vốn không tương thích có thể làm việc cùng nhau".

Ở mức độ nhập môn, Adapter thường được minh họa bằng các ví dụ đơn giản như củ sạc chuyển từ cổng Type-C sang Lightning hoặc ổ cắm 3 chấu sang 2 chấu.

Tuy nhiên, trong kỹ nghệ phần mềm quy mô lớn (Enterprise Software Engineering), Adapter Pattern không chỉ là một thủ thuật bọc code (code wrapper). Nó chính là nền tảng cốt lõi của:

  1. Kiến trúc Lục Giác (Hexagonal Architecture / Ports and Adapters) do Alistair Cockburn sáng lập.
  2. Anti-Corruption Layer (ACL - Lớp chống tha hóa) trong Domain-Driven Design (DDD) của Eric Evans, đóng vai trò "lá chắn sinh tử" khi tích hợp các hệ thống Legacy (ngân hàng lõi Core Banking, SAP ERP, SOAP/XML) với Microservices hiện đại.

Bài viết này sẽ đưa bạn từ bản chất hướng đối tượng của Adapter đến các ứng dụng kiến trúc tối cao trong hệ thống phân tán.


# class Adapter vs object Adapter: bản lĩnh của "composition over inheritance"

Trong Java, GoF định nghĩa hai cách hiện thực hóa Adapter:

# class Adapter (sử dụng kế thừa extends)

  • Do Java không hỗ trợ đa kế thừa lớp (multiple class inheritance), Class Adapter chỉ có thể kế thừa đúng một class cụ thể (Adaptee) và implement interface mục tiêu (Target).
  • Khuyết điểm nghiêm trọng: Nó gắn chặt vào mã nguồn của Adaptee. Bạn không thể dùng adapter này cho các lớp con của Adaptee. Phá vỡ tính đóng gói vì Adapter phơi bày toàn bộ các phương thức public/protected của lớp cha.

# object Adapter (sử dụng ủy quyền composition)

  • Adapter nắm giữ một tham chiếu (reference) tới instance của Adaptee thông qua dependency injection.
  • Ưu điểm vượt trội:
    • Hoàn toàn tuân thủ nguyên lý "Favor object composition over class inheritance".
    • Có thể bọc linh hoạt bất kỳ implementation nào của Adaptee hoặc subclass của nó.
    • Dễ dàng viết Unit Test bằng cách Mock Adaptee.
    • Trong Java hiện đại, 99.9% trường hợp đều sử dụng Object Adapter.

# trái tim của kiến trúc lục giác: Ports & Adapters

Năm 2005, Alistair Cockburn giới thiệu Hexagonal Architecture, và tên gọi nguyên bản chính thức của nó là Ports and Adapters Architecture.

Triết lý nền tảng: Mã nguồn nghiệp vụ (Core Domain) phải nằm ở trung tâm và độc lập 100% với thế giới bên ngoài (Database, HTTP Web, Message Broker, CLI).

# các khái niệm cốt lõi

  1. Port (Cổng Giao Tiếp): Là một Interface do Core Domain sở hữu. Nó đại diện cho mong muốn của Domain:
    • Inbound Port: "Đây là các Use Case tôi cung cấp cho thế giới bên ngoài gọi vào" (e.g. PlaceOrderUseCase).
    • Outbound Port: "Đây là dịch vụ tôi cần thế giới bên ngoài cung cấp cho tôi" (e.g. PaymentGatewayPort, OrderRepositoryPort).
  2. Adapter (Bộ Chuyển Đổi): Nằm ở tầng hạ tầng (Infrastructure). Nó bọc các công nghệ cụ thể và chuyển đổi chúng cho khớp với Port:
    • Inbound Adapter: Nhận HTTP Request JSON từ Spring Web, map sang Domain Command Object rồi gọi vào Inbound Port.
    • Outbound Adapter: Implement Outbound Port, bọc thư viện bên thứ 3 (như Stripe SDK hoặc Hibernate JPA) để chuyển đổi Domain Request thành API calls của Vendor.

# anti-corruption layer (ACL): tấm khiên chống "ô nhiễm" domain

Khi xây dựng một hệ sinh thái Microservices hiện đại để thay thế dần một hệ thống Core Banking hoặc ERP cũ kỹ (Legacy Mainframe), vấn đề lớn nhất không phải là giao thức mạng, mà là Sự lệch pha mô hình dữ liệu (Domain Model Mismatch):

  • Hệ thống cũ dùng chuẩn chuỗi ISO-8583 cố định độ dài, hoặc XML SOAP với hàng trăm trường viết tắt khó hiểu (CUST_STAT_CD_01).
  • Nếu bạn để các Entity của hệ thống mới nhận trực tiếp các field này, toàn bộ mã nguồn sạch của bạn sẽ bị "tha hóa" (corrupted) bởi tư duy thiết kế lỗi thời của 20 năm trước.

# triển khai thực chiến: multi-gateway payment ACL

Hãy xây dựng một hệ thống thanh toán chuẩn production:

  • Domain chỉ biết interface sạch: PaymentGatewayPort.
  • Hai Adapter độc lập: Một bọc Stripe Modern REST SDK, một bọc Legacy Banking Socket System.
# outbound Port thuần khiết của domain
package com.company.billing.domain.port.out;
 
import java.math.BigDecimal;
import java.util.Currency;
 
public interface PaymentGatewayPort {
    PaymentResult executePayment(PaymentOrder order);
}
 
// Pure Domain Records
public record PaymentOrder(String transactionId, BigDecimal amount, Currency currency, String customerToken) {}
public record PaymentResult(boolean isSuccess, String gatewayReference, String errorCode, String rawMessage) {}
# outbound Adapter 1: hiện thực hóa cho Stripe modern API
@Component("stripePaymentAdapter")
public class StripePaymentAdapter implements PaymentGatewayPort {
 
    private final StripeClient stripeClient;
 
    public StripePaymentAdapter(StripeClient stripeClient) {
        this.stripeClient = stripeClient;
    }
 
    @Override
    public PaymentResult executePayment(PaymentOrder order) {
        try {
            // Chuyển đổi Domain Model sang Stripe SDK Parameter Map
            PaymentIntentCreateParams params = PaymentIntentCreateParams.builder()
                .setAmount(order.amount().multiply(BigDecimal.valueOf(100)).longValue()) // Stripe dùng cents
                .setCurrency(order.currency().getCurrencyCode().toLowerCase())
                .setCustomer(order.customerToken())
                .putMetadata("txId", order.transactionId())
                .build();
 
            PaymentIntent intent = stripeClient.paymentIntents().create(params);
 
            // Map ngược từ Vendor Response sang Domain Result
            return new PaymentResult(
                "succeeded".equalsIgnoreCase(intent.getStatus()),
                intent.getId(),
                null,
                intent.getStatus()
            );
        } catch (StripeException e) {
            // Chặn đứng Exception của Vendor không cho rò rỉ vào Domain!
            return new PaymentResult(false, null, e.getCode(), e.getMessage());
        }
    }
}
# outbound Adapter 2: anti-corruption layer bọc socket legacy bank
@Component("legacyBankAdapter")
public class LegacyBankAdapter implements PaymentGatewayPort {
 
    private final LegacySocketClient socketClient;
 
    public LegacyBankAdapter(LegacySocketClient socketClient) {
        this.socketClient = socketClient;
    }
 
    @Override
    public PaymentResult executePayment(PaymentOrder order) {
        try {
            // 1. Dịch thuật sang chuỗi nhị phân cố định byte của Core Banking (ISO-8583 format)
            String fixedLengthPayload = String.format("TX%012d%s%08d",
                order.amount().multiply(BigDecimal.valueOf(100)).longValue(),
                order.currency().getCurrencyCode(),
                Long.parseLong(order.customerToken())
            );
 
            // 2. Gửi qua kết nối TCP Socket thô
            byte[] responseBytes = socketClient.sendAndReceive(fixedLengthPayload.getBytes(StandardCharsets.US_ASCII));
            String responseStr = new String(responseBytes, StandardCharsets.US_ASCII);
 
            // 3. Phân tích chuỗi phản hồi thô của hệ thống cũ
            String responseCode = responseStr.substring(0, 2);
            String approvalCode = responseStr.substring(2, 8);
 
            boolean isSuccess = "00".equals(responseCode);
            return new PaymentResult(isSuccess, approvalCode, responseCode, isSuccess ? "Approved" : "Declined");
        } catch (IOException e) {
            return new PaymentResult(false, null, "NET_ERR", "Lỗi kết nối Socket Core Banking");
        }
    }
}

CAUTION

Anti-Pattern: Leaky Adapter (Rò Rỉ Vendor Khỏi Adapter) Không bao giờ để các ngoại lệ (StripeException, SQLException) hoặc DTO của nhà cung cấp (PaymentIntent) bay xuyên qua Adapter vào Domain. Nếu Domain bắt gặp StripeException, tính độc lập của kiến trúc coi như sụp đổ!


# phân biệt tứ đại structural patterns: Adapter, Facade, Proxy, Decorator

Rất nhiều kỹ sư nhầm lẫn giữa 4 mẫu thiết kế cấu trúc này vì chúng đều liên quan đến việc "bọc một đối tượng khác" (Wrapper):

Mẫu thiết kếThay đổi Interface?Mục đích kiến trúc cốt lõiMối quan hệ đối tượng
AdapterCÓChuyển đổi một giao diện không tương thích thành giao diện mà Client yêu cầu.1 Adaptee → 1 Adapter.
FacadeCÓ (Tạo mới cấp cao)Đơn giản hóa một hệ thống con (subsystem) phức tạp bằng một giao diện tiện lợi duy nhất.Nhiều Subsystems → 1 Facade.
ProxyKHÔNG (Giữ nguyên)Kiểm soát quyền truy cập, lazy loading hoặc định tuyến tới đối tượng thật.Cùng implement chung 1 Interface.
DecoratorKHÔNG (Giữ nguyên)Thêm các trách nhiệm, tính năng bổ sung một cách linh hoạt tại runtime.Lồng nhau vô hạn (Recursive wrapping).

# tổng kết & production checklist

Adapter Pattern không chỉ là một công cụ lập trình hướng đối tượng cơ bản; nó là chìa khóa vàng để thiết lập sự tự do về mặt công nghệ cho hệ thống phần mềm doanh nghiệp:

  • Ngăn ngừa Vendor Lock-in: Đổi cổng thanh toán từ Stripe sang PayPal chỉ là việc viết thêm một PayPalPaymentAdapter, không cần đụng đến một dòng nghiệp vụ.
  • Bảo vệ Domain Bất Biến: Core Domain được giữ trong sáng, thanh lịch, dễ viết Unit Test với tốc độ mili-giây.
  • Dễ Dàng Di Trú Hạ Tầng: Thay thế Database từ Oracle sang PostgreSQL hoặc NoSQL MongoDB chỉ diễn ra bên trong Outbound Persistence Adapter.

Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!

Happy coding 😎 👍🏻 🚀 🔥.

← Previous poststack & queue
Next post →decorator pattern