Java Design Pattern – Adapter

Java Design Pattern – Adapter

·

15 min read

1. Intent

Adapter Pattern là một mẫu thiết kế thuộc nhóm cấu trúc (structural design pattern) trong lập trình phần mềm. Nó được sử dụng để kết nối hai hệ thống hoặc lớp có giao diện không tương thích, bằng cách "chuyển đổi" giao diện của một lớp thành giao diện mà hệ thống khác mong đợi.

Adapter design pattern

2. Problem

Hãy tưởng tượng bạn đang tạo một ứng dụng theo dõi thị trường chứng khoán. Ứng dụng này tải dữ liệu chứng khoán từ nhiều nguồn khác nhau dưới định dạng XML và sau đó hiển thị các biểu đồ và sơ đồ bắt mắt cho người dùng.

Tại một thời điểm, bạn quyết định cải tiến ứng dụng bằng cách tích hợp một thư viện phân tích thông minh từ bên thứ ba. Nhưng có một vấn đề: thư viện phân tích này chỉ hoạt động với dữ liệu ở định dạng JSON.

The structure of the app before integration with the analytics library

You can’t use the analytics library “as is” because it expects the data in a format that’s incompatible with your app.

Bạn có thể thay đổi thư viện để làm việc với XML. Tuy nhiên, điều này có thể làm hỏng một số đoạn mã hiện tại dựa vào thư viện. Tệ hơn nữa, bạn có thể không có quyền truy cập vào mã nguồn của thư viện, khiến cách tiếp cận này trở nên bất khả thi.

3. Solution

Bạn có thể tạo một adapter (bộ chuyển đổi). Đây là một đối tượng đặc biệt có nhiệm vụ chuyển đổi giao diện của một đối tượng để đối tượng khác có thể hiểu được.

Adapter bao bọc (wrap) một trong các đối tượng để ẩn đi sự phức tạp của quá trình chuyển đổi xảy ra ở phía sau. Đối tượng được bao bọc thậm chí không biết đến sự tồn tại của adapter. Ví dụ, bạn có thể bao bọc một đối tượng hoạt động với đơn vị mét và kilomet bằng một adapter để chuyển đổi tất cả dữ liệu sang các đơn vị đo lường imperial như feet và dặm.

Adapter không chỉ có thể chuyển đổi dữ liệu sang các định dạng khác nhau mà còn giúp các đối tượng với giao diện khác biệt có thể hợp tác. Đây là cách hoạt động của adapter:

  1. Adapter cung cấp một giao diện tương thích với một trong các đối tượng hiện có.

  2. Thông qua giao diện này, đối tượng hiện có có thể gọi các phương thức của adapter một cách an toàn.

  3. Khi nhận được một lệnh gọi, adapter chuyển yêu cầu đến đối tượng thứ hai, nhưng theo định dạng và thứ tự mà đối tượng thứ hai mong đợi.

  4. Đôi khi, bạn thậm chí có thể tạo một adapter hai chiều (two-way adapter), cho phép chuyển đổi các lệnh gọi theo cả hai hướng.

    Adapter's solution

Quay lại ứng dụng theo dõi thị trường chứng khoán của bạn. Để giải quyết vấn đề không tương thích định dạng, bạn có thể tạo các adapter chuyển đổi XML sang JSON cho từng lớp của thư viện phân tích mà mã nguồn của bạn làm việc trực tiếp. Sau đó, bạn điều chỉnh mã của mình để chỉ giao tiếp với thư viện thông qua các adapter này. Khi một adapter nhận được lệnh gọi, nó sẽ chuyển đổi dữ liệu XML đầu vào thành cấu trúc JSON và chuyển lệnh gọi đến các phương thức phù hợp của đối tượng phân tích được bao bọc.

4. Real-World Analogy

The Adapter pattern example

Khi bạn lần đầu tiên đi từ Mỹ đến châu Âu, bạn có thể sẽ ngạc nhiên khi cố gắng sạc laptop của mình. Tiêu chuẩn phích cắm và ổ cắm điện ở các quốc gia khác nhau là khác nhau. Đó là lý do tại sao phích cắm kiểu Mỹ của bạn không vừa với ổ cắm kiểu Đức. Vấn đề này có thể được giải quyết bằng cách sử dụng một adapter phích cắm điện, có ổ cắm kiểu Mỹ và phích cắm kiểu châu Âu.

5. Structure

Có hai cách để thực hiện Adapter Pattern dựa theo cách cài đặt (implement) của chúng:

Object adapter - Composition

Cách triển khai này sử dụng nguyên tắc object composition: adapter triển khai giao diện của một đối tượng và bao bọc đối tượng khác. Nó có thể được triển khai trong tất cả các ngôn ngữ lập trình phổ biến.

Structure of the Adapter design pattern (the object adapter)

Các thành phần chính:

  • The Client is a class that contains the existing business logic of the program

  • Client Interface mô tả một giao thức mà các lớp khác phải tuân theo để có thể collaborate )(tích hợp) với code của client.

  • Service là một class cần sử dụng (thường là từ bên thứ ba hoặc legacy - mã nguồn cũ). Client không thể sử dụng lớp này trực tiếp vì nó có một giao diện không tương thích.

  • Adapter là một lớp có khả năng làm việc với cả client và service: nó triển khai giao diện của client đồng thời bao bọc đối tượng service. Adapter nhận các lệnh gọi từ client thông qua giao diện của client và chuyển đổi chúng thành các lệnh gọi đến đối tượng service được bao bọc theo định dạng mà service có thể hiểu được.

  • Mã client không bị ràng buộc với lớp adapter cụ thể miễn là nó làm việc với adapter thông qua giao diện của client. Nhờ đó, bạn có thể thêm các loại adapter mới vào chương trình mà không làm ảnh hưởng đến mã client hiện có. Điều này rất hữu ích khi giao diện của lớp service thay đổi hoặc được thay thế: bạn chỉ cần tạo một lớp adapter mới mà không cần chỉnh sửa mã của client.

Class adapter - Inheritance

Cách triển khai này sử dụng kế thừa: adapter kế thừa giao diện từ cả hai đối tượng cùng một lúc. Lưu ý rằng cách tiếp cận này chỉ có thể được thực hiện trong các ngôn ngữ lập trình hỗ trợ đa kế thừa, chẳng hạn như C++.

Adapter design pattern (class adapter)

Class Adapter không cần bao bọc bất kỳ đối tượng nào vì nó kế thừa các hành vi từ cả client và service. Việc thích nghi được thực hiện trong các phương thức ghi đè. Adapter kết quả có thể được sử dụng thay thế cho một lớp client hiện có.

6. Pseudocode

This example of the Adapter pattern is based on the classic conflict between square pegs and round holes.

Structure of the Adapter pattern example

// Say you have two classes with compatible interfaces:
// RoundHole and RoundPeg.
class RoundHole is
    constructor RoundHole(radius) { ... }

    method getRadius() is
        // Return the radius of the hole.

    method fits(peg: RoundPeg) is
        return this.getRadius() >= peg.getRadius()

class RoundPeg is
    constructor RoundPeg(radius) { ... }

    method getRadius() is
        // Return the radius of the peg.

// But there's an incompatible class: SquarePeg.
class SquarePeg is
    constructor SquarePeg(width) { ... }

    method getWidth() is
        // Return the square peg width.

// An adapter class lets you fit square pegs into round holes.
// It extends the RoundPeg class to let the adapter objects act
// as round pegs.
class SquarePegAdapter extends RoundPeg is
    // In reality, the adapter contains an instance of the
    // SquarePeg class.
    private field peg: SquarePeg

    constructor SquarePegAdapter(peg: SquarePeg) is
        this.peg = peg

    method getRadius() is
        // The adapter pretends that it's a round peg with a
        // radius that could fit the square peg that the adapter
        // actually wraps.
        return peg.getWidth() * Math.sqrt(2) / 2

// Somewhere in client code.
hole = new RoundHole(5)
rpeg = new RoundPeg(5)
hole.fits(rpeg) // true

small_sqpeg = new SquarePeg(5)
large_sqpeg = new SquarePeg(10)
hole.fits(small_sqpeg) // this won't compile (incompatible types)

small_sqpeg_adapter = new SquarePegAdapter(small_sqpeg)
large_sqpeg_adapter = new SquarePegAdapter(large_sqpeg)
hole.fits(small_sqpeg_adapter) // true
hole.fits(large_sqpeg_adapter) // false

7. Applicability

  • Sử dụng lớp Adapter khi bạn muốn sử dụng một lớp hiện có, nhưng giao diện của nó không tương thích với phần còn lại của mã.

    Mẫu Adapter cho phép bạn tạo một lớp trung gian đóng vai trò như một trình dịch giữa mã của bạn và một lớp kế thừa, một lớp của bên thứ ba hoặc bất kỳ lớp nào khác có giao diện lạ.

  • Sử dụng mẫu này khi bạn muốn tái sử dụng một số lớp con hiện có thiếu một số chức năng chung mà không thể thêm vào lớp cha.

    Bạn có thể mở rộng từng lớp con và đưa chức năng bị thiếu vào các lớp con mới. Tuy nhiên, bạn sẽ cần phải sao chép mã trên tất cả các lớp mới này, điều này thực sự không tốt.

    Giải pháp tốt hơn nhiều là đưa chức năng bị thiếu vào một lớp adapter. Sau đó, bạn sẽ bao bọc các đối tượng có các tính năng bị thiếu bên trong adapter, từ đó có được các tính năng cần thiết một cách linh hoạt. Để làm được điều này, các lớp đích phải có một giao diện chung và trường của adapter phải tuân theo giao diện đó. Cách tiếp cận này trông rất giống với mẫu Decorator.

So sánh với Decorator Pattern:

  • Điểm tương đồng:

    • Cả Adapter và Decorator đều bao bọc (wrap) một đối tượng khác để thêm hành vi hoặc tính năng.

    • Cả hai đều có thể được triển khai một cách linh hoạt và mở rộng mà không ảnh hưởng đến các lớp hiện có.

  • Khác biệt chính:

    • Adapter: Dùng để chuyển đổi giao diện không tương thích giữa hai lớp.

    • Decorator: Dùng để mở rộng chức năng của một đối tượng mà không thay đổi giao diện của nó.

Adapter Pattern là một công cụ mạnh mẽ khi làm việc với mã cũ hoặc các hệ thống tích hợp, cho phép tăng khả năng tái sử dụng và giảm sự phụ thuộc vào việc sửa đổi mã gốc.

8. How to Implement

Các bước triển khai (Implement) Adapter Pattern:

  1. Xác định hai lớp có giao diện không tương thích:

    • Một lớp dịch vụ (service class) hữu ích mà bạn không thể thay đổi (thường là từ bên thứ ba, mã cũ hoặc có nhiều phụ thuộc hiện có).

    • Một hoặc nhiều lớp client sẽ hưởng lợi từ việc sử dụng lớp dịch vụ.


  1. Khai báo client interface:

    • Định nghĩa client interface (client interface), mô tả cách các lớp client giao tiếp với lớp dịch vụ.

  1. Tạo lớp Adapter:

    • Tạo lớp adapter và đảm bảo nó tuân theo client interface.

    • Ban đầu, để trống tất cả các phương thức trong lớp adapter.


  1. Thêm trường để tham chiếu đến đối tượng dịch vụ:

    • Trong lớp adapter, thêm một trường để lưu tham chiếu đến đối tượng dịch vụ.

    • Thông thường, trường này được khởi tạo thông qua constructor của adapter. Tuy nhiên, trong một số trường hợp, có thể thuận tiện hơn nếu truyền tham chiếu đối tượng dịch vụ qua các phương thức của adapter khi cần sử dụng.


  1. Triển khai các phương thức trong client interface:

    • Lần lượt triển khai tất cả các phương thức của client interface trong lớp adapter.

    • Lớp adapter nên ủy quyền hầu hết công việc thực tế cho đối tượng dịch vụ, chỉ xử lý phần chuyển đổi giao diện hoặc định dạng dữ liệu.


  1. Sử dụng adapter qua client interface:

    • Các lớp client nên sử dụng adapter thông qua client interface.

    • Điều này cho phép bạn thay đổi hoặc mở rộng lớp adapter mà không ảnh hưởng đến mã của client.


Ví dụ minh họa:

Giả sử bạn có:

  • Service class: LegacyPrinter (không thay đổi được).

  • Client: Muốn sử dụng Printer thông qua giao diện Printable.

Triển khai Adapter Pattern:

// client interface
interface Printable {
    void print(String text);
}

// Lớp dịch vụ (Service class)
class LegacyPrinter {
    void legacyPrint(String text) {
        System.out.println("Legacy Printer: " + text);
    }
}

// Lớp Adapter
class PrinterAdapter implements Printable {
    private LegacyPrinter legacyPrinter;

    // Constructor nhận đối tượng LegacyPrinter
    public PrinterAdapter(LegacyPrinter legacyPrinter) {
        this.legacyPrinter = legacyPrinter;
    }

    // Chuyển đổi giao diện
    @Override
    public void print(String text) {
        legacyPrinter.legacyPrint(text); // Ủy quyền công việc cho lớp dịch vụ
    }
}

// Client sử dụng Adapter
class Client {
    public static void main(String[] args) {
        LegacyPrinter legacyPrinter = new LegacyPrinter();
        Printable adapter = new PrinterAdapter(legacyPrinter); // Sử dụng Adapter
        adapter.print("Hello, World!"); // Gọi phương thức theo client interface
    }
}

Kết quả:

  • Adapter đã chuyển đổi giao diện của LegacyPrinter để tương thích với giao diện Printable, cho phép các client sử dụng dịch vụ mà không cần sửa đổi lớp dịch vụ gốc.

  • Lớp client chỉ làm việc với client interface (Printable), giúp mã dễ bảo trì và mở rộng.

9. Pros and Cons

Pros

  1. Single Responsibility Principle:

    • Adapter giúp tách rời mã xử lý chuyển đổi giao diện hoặc dữ liệu khỏi logic nghiệp vụ chính của chương trình.

    • Điều này làm cho mã trở nên rõ ràng hơn, dễ bảo trì và mở rộng.

  2. Open/Closed Principle:

    • Bạn có thể dễ dàng giới thiệu các loại adapter mới vào chương trình mà không làm ảnh hưởng đến mã client hiện có, miễn là client tương tác thông qua giao diện client (client interface).

    • Điều này đặc biệt hữu ích khi cần hỗ trợ nhiều lớp dịch vụ khác nhau với các giao diện không đồng nhất.

  3. Tăng khả năng tái sử dụng:

    • Adapter cho phép sử dụng lại các lớp hiện có (thường là lớp cũ hoặc bên thứ ba) mà không cần sửa đổi mã nguồn của chúng.
  4. Tính linh hoạt cao:

    • Adapter giúp các lớp có giao diện không tương thích có thể cộng tác với nhau mà không cần thay đổi cấu trúc hiện có.

Cons

  1. Tăng độ phức tạp của code:

    • Adapter yêu cầu giới thiệu thêm các interface và class mới, làm tăng tổng thể độ phức tạp của hệ thống.

    • Trong các trường hợp đơn giản, việc thay đổi trực tiếp lớp dịch vụ để tương thích với mã hiện tại có thể là giải pháp dễ dàng hơn.

  2. Chi phí hiệu năng:

    • Mặc dù không đáng kể trong hầu hết các trường hợp, việc sử dụng Adapter có thể làm tăng chi phí xử lý khi phải chuyển đổi giao diện hoặc dữ liệu.
  3. Phụ thuộc vào giao diện chung:

    • Để áp dụng Adapter Pattern, các lớp cần sử dụng một giao diện chung, điều này có thể hạn chế nếu hệ thống không hỗ trợ điều này từ trước.

Khi nào nên và không nên sử dụng Adapter Pattern:

  • Nên sử dụng:

    • Khi bạn cần tích hợp các lớp hoặc thư viện có giao diện không tương thích.

    • Khi không thể hoặc không muốn thay đổi mã của lớp dịch vụ (do bên thứ ba, mã cũ, hoặc có nhiều phụ thuộc).

    • Khi muốn tuân thủ nguyên tắc thiết kế tốt, như tách biệt logic nghiệp vụ và xử lý giao diện.

  • Không nên sử dụng:

    • Khi có thể dễ dàng thay đổi mã của lớp dịch vụ để làm cho nó tương thích với hệ thống.

    • Khi việc sử dụng Adapter làm tăng độ phức tạp không cần thiết cho hệ thống.

10. Relations with Other Patterns

Adapter vs. Bridge:

  • Bridge:

    • Thường được thiết kế từ đầu (up-front design) để giúp phát triển các phần của ứng dụng một cách độc lập.

    • Giải quyết vấn đề tách giao diện (interface) khỏi phần triển khai (implementation).

  • Adapter:

    • Thường được sử dụng trong các ứng dụng đã có sẵn (existing apps) để làm cho các lớp không tương thích có thể hoạt động cùng nhau.

    • Chỉ tập trung vào việc chuyển đổi giao diện để các đối tượng có thể cộng tác.


Adapter vs. Decorator:

  • Adapter:

    • Cung cấp một giao diện hoàn toàn khác để truy cập một đối tượng hiện có.

    • Không hỗ trợ recursive composition (bao bọc nhiều lớp lồng nhau).

  • Decorator:

    • Giao diện vẫn giữ nguyên hoặc được mở rộng, không thay đổi hoàn toàn.

    • Hỗ trợ recursive composition, cho phép thêm nhiều hành vi động vào một đối tượng.


Adapter vs. Proxy:

  • Adapter:

    • Truy cập đối tượng hiện có thông qua một giao diện khác.

    • Mục tiêu là làm cho giao diện của một lớp phù hợp với hệ thống hiện tại.

  • Proxy:

    • Giao diện giữ nguyên, giống như đối tượng thực sự.

    • Chủ yếu được sử dụng để kiểm soát quyền truy cập, tối ưu hóa hoặc bảo vệ đối tượng thực sự.


Adapter vs. Facade:

  • Facade:

    • Định nghĩa một giao diện hoàn toàn mới cho một nhóm các đối tượng hoặc hệ thống con.

    • Làm giảm độ phức tạp của hệ thống bằng cách ẩn các chi tiết triển khai.

    • Thường hoạt động với một hệ thống con gồm nhiều đối tượng.

  • Adapter:

    • Làm cho giao diện hiện có của một đối tượng có thể sử dụng được mà không thay đổi nó.

    • Thường chỉ bao bọc một đối tượng duy nhất.


Cấu trúc tương tự giữa Adapter, Bridge, State, Strategy:

  • Cấu trúc chung:

    • Cả bốn mẫu này đều dựa trên nguyên tắc composition (ủy quyền công việc cho các đối tượng khác).
  • Điểm khác biệt:

    • Adapter: Giải quyết vấn đề tương thích giao diện.

    • Bridge: Tách biệt giao diện và triển khai để chúng có thể phát triển độc lập.

    • State: Thay đổi hành vi của một đối tượng dựa trên trạng thái hiện tại.

    • Strategy: Cho phép thay thế các thuật toán động trong khi chương trình đang chạy.

  • Ý nghĩa của pattern:

    • Không chỉ là một công thức để tổ chức mã mà còn giúp truyền đạt ý định thiết kế (design intent) và vấn đề mà pattern giải quyết cho các lập trình viên khác.

Tóm tắt:

Adapter pattern có nhiều điểm tương đồng với các mẫu khác như Decorator, Proxy, Facade, Bridge... nhưng mục tiêu của Adapter luôn là làm cho các lớp không tương thích có thể làm việc cùng nhau thông qua việc chuyển đổi giao diện. Sự khác biệt chính nằm ở mục tiêu và ngữ cảnh sử dụng của từng pattern.

Code Examples

References: https://refactoring.guru/design-patterns/adapter