Nhận Xét Không Chịu Trách Nhiệm Về COM
Dưới đây là bài viết suy ngẫm về COM mà tôi đã viết trước đó trên cuốn sổ lưu bút cũ:
Tại sao lại có COM? Theo cách hiểu của tôi, Microsoft muốn thiết lập một tiêu chuẩn cho việc biểu diễn nhị phân của các đối tượng, nhằm mục đích tái sử dụng các module nhị phân. Vào thời điểm COM ra đời, C++ là ngôn ngữ phù hợp nhất để thực hiện mô hình hóa đối tượng. Cho đến tận ngày nay, C++ vẫn là công cụ tối ưu nhất để tạo ra mã máy gốc (native code) theo phương pháp hướng đối tượng một cách trực tiếp. Tiếc thay, phương pháp triển khai C++ lại thiếu sự thống nhất - từ quy tắc bố trí dữ liệu trong đối tượng, cách truyền tham số khi gọi hàm, xử lý giá trị trả về, cho đến cơ chế đa kế thừa đều không có chuẩn mực chung.
Khi thiết kế COM, Microsoft áp dụng nguyên tắc tối giản: chỉ cần thống nhất cấu trúc bảng ảo (virtual table) - thành phần tối thiểu để đảm bảo tính tương thích giữa các trình biên dịch C++. COM quy định bắt buộc con trỏ đến bảng ảo phải nằm tại 4 byte đầu tiên của địa chỉ vật lý đối tượng, ngoài ra không yêu cầu gì thêm. Từ đây hình thành nên hai nguyên tắc nổi bật của COM:
- Tất cả các interface phải là lớp trừu tượng thuần túy - không cho phép tiết lộ biến thành viên bên trong đối tượng, không chứa hàm không ảo. Điều này đảm bảo mọi địa chỉ hàm đều có thể truy xuất qua bảng ảo.
- Cấm đa kế thừa interface, nhằm bảo đảm tính duy nhất của bảng ảo trong mỗi interface.
Tuy nhiên, lớp triển khai đối tượng có thể đa kế thừa nhiều interface. Để giải quyết vấn đề chuyển đổi giữa các interface này, COM đưa vào phương thức QueryInterface trong IUnknown - cơ chế khắc phục sự khác biệt trong cách triển khai đa kế thừa của các compiler C++ khác nhau.
Ví dụ với đoạn mã:
|
|
Trên các compiler khác nhau, giá trị của a và b có thể khác nhau do cách điều chỉnh địa chỉ khi chuyển đổi kiểu. Khi dùng con trỏ C*, A* hay B* trỏ đến cùng một đối tượng C, địa chỉ vật lý có thể không giống nhau. Chỉ khi có cơ chế chuyển đổi chuẩn như QueryInterface, mới đảm bảo truy cập chính xác giữa các interface.
QueryInterface thực chất mang lại cho COM khả năng tương tự dynamic_cast trong C++. Vì vậy COM yêu cầu QueryInterface phải có tính đối xứng: nếu từ interface A có thể QueryInterface thành công sang B, thì ngược lại từ B cũng phải QueryInterface được về A. Miễn là thuộc cùng một đối tượng, bất kỳ cặp interface nào cũng phải chuyển đổi được cho nhau.
Vậy tại sao IUnknown lại bắt buộc phải có AddRef và Release? Trong C++, đối tượng không cần phải tự quản lý bộ đếm tham chiếu. Lý do COM đưa vào yêu cầu này nằm ở khả năng một đối tượng cung cấp nhiều interface không liên quan. Giả sử đối tượng đồng thời có interface “Mèo” và “Chó”, khi hai thành phần này độc lập ngang hàng, việc xác định ai chịu trách nhiệm giải phóng đối tượng sẽ gây mâu thuẫn. Cơ chế đếm tham chiếu giúp giải quyết vấn đề này bằng cách quản lý vòng đời đối tượng dựa trên tổng số tham chiếu đang tồn tại.
Ba phương thức bắt buộc trong IUnknown (QueryInterface, AddRef, Release) thực sự là cách mô phỏng đối tượng C++ một cách tinh gọn nhất. Tuy nhiên trong thực tiễn, cấu trúc này không phải lúc nào cũng hợp lý:
- Với các đối tượng chỉ triển khai đơn interface, việc hỗ trợ QueryInterface trở nên thừa thãi. Tương tự, cơ chế đếm tham chiếu cũng không cần thiết nếu đối tượng được quản lý bởi một thực thể bên ngoài.
- Có thể chia đối tượng thành hai loại: loại singleton (luôn chỉ tồn tại một thể hiện) và loại được tạo ra từ factory. Với loại singleton, việc đếm tham chiếu từ bên ngoài sẽ đơn giản hơn quản lý bên trong. Với loại factory, việc tạo dựng và hủy bỏ nên do cùng một cơ chế đảm nhiệm, chứ không nên tách rời như mô hình COM hiện tại (Factory tạo đối tượng, đối tượng tự hủy qua Release).
Trong quá trình thiết kế kiến trúc engine game mới, tôi ban đầu mô phỏng theo COM nhưng ngày càng nhận ra những bất cập cần điều chỉnh. Đặc biệt sau khi đọc lại bài viết tham khảo của Meng Yan () và trải qua đêm trắng suy ngẫm, tôi đã quyết định xây dựng một hệ thống mới phù hợp hơn với yêu cầu thực tế.
P/s: Cảm ơn anh Meng Yan đã cung cấp tài liệu tham khảo quý giá. Ban đầu đọc qua tôi chưa thấu đáo, nhưng sau khi nghiền ngẫm kỹ càng, mới thực sự hiểu sâu sắc những vấn đề tưởng chừng quen thuộc.