Adapter Pattern là gì?
Adapter cho phép giao tiếp giữa 2 interface không tương thích bằng cách bao bọc (wrap) một lớp hiện tại bằng một lớp adapter để nó có thể sử dụng interface mong muốn.
Ví dụ minh họa:
Giả sử bạn có một interface MediaPlayer chỉ phát nhạc .mp3, nhưng bạn lại muốn phát .mp4 hoặc .vlc, bạn có thể tạo một MediaAdapter để chuyển đổi định dạng đó.
Cấu trúc Adapter Pattern
Client -> Target Interface <- Adapter -> Adaptee
- Target: Interface mà client mong đợi.
- Adaptee: Lớp có sẵn nhưng interface không tương thích.
- Adapter: Cầu nối giúp Adaptee tương thích với Target.
Ví dụ Java đơn giản:
// Target
interface MediaPlayer { void play(String audioType, String fileName);
} // Adaptee
class AdvancedMediaPlayer { void playVlc(String fileName) { System.out.println("Playing vlc: " + fileName); }
} // Adapter
class MediaAdapter implements MediaPlayer { AdvancedMediaPlayer advancedMusicPlayer = new AdvancedMediaPlayer(); public void play(String audioType, String fileName) { if(audioType.equalsIgnoreCase("vlc")) { advancedMusicPlayer.playVlc(fileName); } }
}
Ưu điểm:
- Tái sử dụng mã nguồn có sẵn.
- Tách biệt logic tích hợp khỏi logic xử lý chính.
- Tăng tính mở rộng (open for extension).
Nhược điểm:
- Gây thêm một lớp trừu tượng.
- Có thể phức tạp nếu adapter phải xử lý nhiều loại chuyển đổi khác nhau.
Ứng dụng thực tế trong Spring Boot
Trường hợp sử dụng phổ biến:
- Tích hợp hệ thống cũ (legacy system) vào hệ thống mới.
- Chuyển đổi DTO/Entity khác nhau giữa các microservices.
- Bọc API bên thứ ba (3rd-party) để dùng theo chuẩn riêng.
- Chuyển đổi dữ liệu giữa các tầng: ví dụ adapter giữa DAO và service layer.
Tình huống thực tế:
- Bạn có một hệ thống Spring Boot nhận yêu cầu tìm kiếm thông tin khách hàng.
- Bạn phải gọi tới một API bên ngoài (vd: http://third-party.com/api/customer?id=...) để lấy thông tin khách hàng.
- API bên ngoài trả về định dạng không khớp với DTO nội bộ.
Định nghĩa cấu trúc dữ liệu
Giả sử response từ API bên ngoài:
{ "full_name": "Nguyen Van A", "phone_number": "0123456789", "email_address": "nva@example.com"
}
DTO nội bộ mong muốn:
public class CustomerInfo { private String name; private String phone; private String email;
}
Tạo lớp Adaptee (dữ liệu từ bên ngoài)
Tạo lớp Adapter:
public class ExternalCustomerResponse { @JsonProperty("full_name") private String fullName; @JsonProperty("phone_number") private String phoneNumber; @JsonProperty("email_address") private String emailAddress; // getters, setters
}
Tạo lớp Adapter
// Adapter
public class CustomerAdapter { public static CustomerInfo convert(ExternalCustomerResponse external) { CustomerInfo info = new CustomerInfo(); info.setName(external.getFullName()); info.setPhone(external.getPhoneNumber()); info.setEmail(external.getEmailAddress()); return info; }
}
Gọi API bên ngoài và sử dụng Adapter
@Service
public class CustomerService { private final RestTemplate restTemplate; public CustomerService(RestTemplateBuilder builder) { this.restTemplate = builder.build(); } public CustomerInfo getCustomerById(String id) { String url = "http://third-party.com/api/customer?id=" + id; ExternalCustomerResponse response = restTemplate.getForObject(url, ExternalCustomerResponse.class); return CustomerAdapter.convert(response); // Adapter dùng ở đây }
}
Controller sử dụng CustomerInfo nội bộ
@RestController
@RequestMapping("/customers")
public class CustomerController { private final CustomerService customerService; public CustomerController(CustomerService service) { this.customerService = service; } @GetMapping("/{id}") public ResponseEntity<CustomerInfo> getCustomer(@PathVariable String id) { return ResponseEntity.ok(customerService.getCustomerById(id)); }
}
Có thể sử dụng ModelMapper, hoặc những thư viện khác hỗ trợ việc mapping. Còn nếu các thuộc tính trùng nhau về tên thì có thể sử dụng BeanUtils.copyProperties
của Spring
https://www.baeldung.com/java-performance-mapping-frameworks
Tìm hiểu thêm và đưa ra quyết định link phía trên nhé.
So sánh hiệu năng (thời gian trung bình để map 1 object)
Thư viện | Hiệu năng (thấp hơn là tốt hơn) |
---|---|
MapStruct | Tốt nhất |
JMapper | Rất tốt |
Orika | Trung bình |
ModelMapper | Chậm |
Dozer | Rất chậm |
MapStruct vượt trội về hiệu năng vì nó generate code tại compile-time, không dùng reflection.
Đặc điểm từng thư viện
- MapStruct: Hiệu năng cao, compile-time check, cần generate code nhưng rất phù hợp cho dự án lớn.
- ModelMapper: Dễ dùng, mapping tự động, nhưng chậm hơn do dùng reflection.
- Orika: Hỗ trợ mapping phức tạp, hiệu năng ổn định.
- Dozer: Dễ dùng nhưng hiệu năng thấp, ít được cập nhật.
- JMapper: Hiệu năng cao, nhưng cấu hình phức tạp hơn.
Gợi ý lựa chọn
- Dự án lớn, cần hiệu năng cao: Nên dùng MapStruct.
- Dự án nhỏ, cần triển khai nhanh: Có thể dùng ModelMapper.
- Cần mapping phức tạp: Xem xét Orika hoặc JMapper.