[Độ khó học tập: ★★☆☆☆, Tần suất sử dụng: ★★★★☆]
Một máy tính xách tay thường hoạt động ở điện áp 20V, trong khi mạng điện dân dụng tại Việt Nam cung cấp 220V. Để thiết bị 20V có thể vận hành an toàn trên nguồn 220V, ta cần một bộ chuyển đổi nguồn (AC adapter) — hay còn gọi là cục sạc — nhằm "dịch" điện áp đầu vào sao cho phù hợp. Bộ chuyển đổi này đóng vai trò cầu nối giữa hai hệ thống có đặc tính khác biệt, như minh họa trong Hình 9-1.
Hình 9-1: Sơ đồ nguyên lý bộ chuyển đổi nguồn
Trong phát triển phần mềm, tình huống tương tự cũng thường xảy ra: các thành phần đã tồn tại có giao diện không khớp với yêu cầu của hệ thống mới — chẳng hạn do tên phương thức, tham số, hoặc kiểu trả về khác nhau. Khi không thể sửa đổi mã nguồn gốc, giải pháp tối ưu là chèn thêm một lớp trung gian — gọi là adapter — để "dịch" lời gọi từ phía người dùng sang định dạng mà thành phần cũ hiểu được. Đây chính là bản chất của mô hình Adapter.
9.1 Vấn đề thực tế: Thư viện thuật toán không có mã nguồn
Công ty phần mềm Sunny từng phát triển một thư viện thuật toán gồm các hàm sắp xếp và tìm kiếm hiệu năng cao. Trong dự án hệ thống quản lý đào tạo cho một trường đại học, nhóm phát triển cần hỗ trợ sắp xếp và tra cứu điểm số sinh viên. Họ đã định nghĩa một giao diện chuẩn:
interface ScoreProcessor {
int[] arrange(int[] scores);
boolean locate(int[] scores, int target);
}
Tuy nhiên, thư viện cũ chỉ cung cấp hai lớp độc lập:
FastSortervới phương thứcperformQuickSort(int[])BinaryLocatorvới phương thứcfindInSortedArray(int[], int)
Do mất mã nguồn và không thể chỉnh sửa trực tiếp thư viện, đồng thời giao diện ScoreProcessor đã được sử dụng rộng rãi trong hệ thống, việc thay đổi nó sẽ gây ảnh hưởng dây chuyền. Bài toán đặt ra là: Làm thế nào để tích hợp thư viện cũ vào hệ thống mới mà không sửa mã hiện có, không cần source code, và vẫn tuân thủ nguyên tắc mở–đóng (Open/Closed Principle)?
Giải pháp nằm ở việc xây dựng một lớp điều phối — giống như bộ chuyển đổi điện — để che giấu sự bất tương thích giữa ScoreProcessor (giao diện mong muốn) và các lớp thuật toán cũ (giao diện hiện có).
9.2 Tổng quan về mô hình Adapter
Mô hình Adapter thuộc nhóm mô hình cấu trúc, có hai dạng triển khai chính:
- Object Adapter: Dùng quan hệ kết hợp (composition) — adapter giữ tham chiếu đến adaptee.
- Class Adapter: Dùng kế thừa (inheritance) — adapter mở rộng adaptee (chỉ khả thi nếu ngôn ngữ hỗ trợ đa kế thừa hoặc kế thừa từ lớp cụ thể).
Trong Java, do giới hạn về đơn kế thừa, Object Adapter được ưa chuộng hơn. Cấu trúc cơ bản gồm ba thành phần:
- Target: Giao diện hoặc lớp trừu tượng mà client mong đợi.
- Adaptee: Lớp hiện có, có chức năng cần tái sử dụng nhưng giao diện không khớp.
- Adapter: Lớp trung gian triển khai
Target, đồng thời chứa hoặc mở rộngAdaptee, để chuyển hướng lời gọi.
Dưới đây là ví dụ minh họa Object Adapter theo cách tiếp cận hiện đại, sử dụng constructor injection và tên biến mang tính biểu đạt cao:
// Giao diện đích — tiêu chuẩn hệ thống
interface ScoreProcessor {
int[] arrange(int[] scores);
boolean locate(int[] scores, int target);
}
// Thành phần cũ — không thể sửa
class FastSorter {
public int[] performQuickSort(int[] data) {
if (data == null || data.length <= 1) return data;
quickSortHelper(data, 0, data.length - 1);
return data;
}
private void quickSortHelper(int[] arr, int low, int high) {
if (low < high) {
int pivotIndex = partition(arr, low, high);
quickSortHelper(arr, low, pivotIndex - 1);
quickSortHelper(arr, pivotIndex + 1, high);
}
}
private int partition(int[] arr, int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
swap(arr, i, j);
}
}
swap(arr, i + 1, high);
return i + 1;
}
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
class BinaryLocator {
public boolean findInSortedArray(int[] sorted, int key) {
int left = 0, right = sorted.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (sorted[mid] == key) return true;
else if (sorted[mid] < key) left = mid + 1;
else right = mid - 1;
}
return false;
}
}
// Adapter — lớp điều phối
class ScoreProcessorAdapter implements ScoreProcessor {
private final FastSorter sortingEngine;
private final BinaryLocator searchingEngine;
public ScoreProcessorAdapter() {
this.sortingEngine = new FastSorter();
this.searchingEngine = new BinaryLocator();
}
@Override
public int[] arrange(int[] scores) {
return sortingEngine.performQuickSort(scores.clone()); // Tránh side effect
}
@Override
public boolean locate(int[] scores, int target) {
return searchingEngine.findInSortedArray(scores, target);
}
}
9.3 Triển khai linh hoạt với cấu hình động
Để tăng tính mở rộng, hệ thống sử dụng file cấu hình XML và phản xạ (reflection) để tải adapter tại thời điểm chạy — giúp thay đổi chiến lược xử lý mà không cần biên dịch lại mã nguồn.
Tệp cấu hình config.xml:
<?xml version="1.0" encoding="UTF-8"?>
<system-config>
<adapter-class>ScoreProcessorAdapter</adapter-class>
</system-config>
Lớp tiện ích đọc cấu hình:
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import java.io.File;
public class ConfigLoader {
public static Class<?> loadAdapterClass() {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new File("config.xml"));
doc.getDocumentElement().normalize();
NodeList nodes = doc.getElementsByTagName("adapter-class");
if (nodes.getLength() > 0) {
String className = nodes.item(0).getTextContent().trim();
return Class.forName(className);
}
} catch (Exception e) {
throw new RuntimeException("Failed to load adapter config", e);
}
return ScoreProcessorAdapter.class;
}
}
Chương trình kiểm thử:
public class SystemTest {
public static void main(String[] args) {
try {
Class<?> adapterClass = ConfigLoader.loadAdapterClass();
ScoreProcessor processor = (ScoreProcessor) adapterClass.getDeclaredConstructor().newInstance();
int[] rawScores = {84, 76, 50, 69, 90, 91, 88, 96};
System.out.println("Danh sách điểm sau sắp xếp:");
int[] sorted = processor.arrange(rawScores);
System.out.println(java.util.Arrays.toString(sorted));
System.out.println("Tìm kiếm điểm 90: " + processor.locate(sorted, 90));
System.out.println("Tìm kiếm điểm 92: " + processor.locate(sorted, 92));
} catch (Exception e) {
e.printStackTrace();
}
}
}
9.4 So sánh hai dạng Adapter
Class Adapter (ít dùng trong Java):
class LegacyAdapter extends BinaryLocator implements ScoreProcessor {
@Override
public int[] arrange(int[] scores) {
// Không thể triển khai sắp xếp vì chỉ kế thừa từ BinaryLocator
throw new UnsupportedOperationException("Sorting not supported");
}
@Override
public boolean locate(int[] scores, int target) {
return findInSortedArray(scores, target); // Kế thừa trực tiếp
}
}
Ưu điểm: Đơn giản, ít lớp trung gian.
Nhược điểm: Phụ thuộc vào khả năng kế thừa; không thể kết hợp nhiều adaptee; vi phạm nguyên tắc "ưu tiên composition hơn inheritance".
9.5 Trường hợp đặc biệt: Adapter hai chiều
Khi adapter vừa triển khai Target, vừa giữ tham chiếu tới Adaptee, và cho phép cả hai bên gọi qua nhau — ta có two-way adapter. Mẫu này hiếm khi dùng do làm tăng độ phức tạp và rủi ro vòng lặp gọi đệ quy.
9.6 Tổng kết ứng dụng
Ưu điểm chính:
- Giải ghép hoàn toàn giữa client và thư viện cũ.
- Hỗ trợ mở rộng dễ dàng: chỉ cần thêm adapter mới, không sửa mã hiện hữu.
- Tăng tính tái sử dụng: cùng một adaptee có thể được bao bọc bởi nhiều adapter cho các ngữ cảnh khác nhau.
Hạn chế:
- Object Adapter: Khó ghi đè hành vi của adaptee (cần lớp con trung gian nếu bắt buộc).
- Class Adapter: Không khả thi trong Java nếu adaptee là
finalhoặcTargetlà lớp chứ không phải interface.
Thời điểm áp dụng:
- Khi tích hợp thư viện bên ngoài (third-party), driver, hoặc legacy code không có source.
- Khi cần chuẩn hóa giao diện cho nhiều thành phần không liên quan.
- Khi thiết kế hệ thống hướng tới khả năng cấu hình và thay thế linh hoạt.