Nhìn Từ Góc Độ Mẫu Command: Hạn Chế Của C++
Trong lĩnh vực thiết kế phần mềm có giao diện người dùng, mẫu Command có thể coi là một trong những mẫu thiết kế phổ biến nhất. Mẫu này giúp ẩn giấu chi tiết về đối tượng khởi phát lệnh và quy trình xử lý lệnh, tạo ra một đối tượng trung gian làm nhiệm vụ tách biệt mối liên kết giữa người gửi và người nhận.
Ứng dụng tiêu biểu nhất của mẫu Command chính là triển khai chức năng undo/redo. Chỉ cần duy trì một danh sách liên kết chứa chuỗi lệnh đã thực thi, ta có thể dễ dàng ghi nhớ các lệnh đã phát ra. Mỗi đối tượng Command cần triển khai cơ chế chuyển đổi hai chiều (hoặc một cặp phương thức undo/redo), đồng thời lưu trữ các đối tượng liên quan bên trong để tự thực hiện thao tác hoàn tác khi cần.
Khi sử dụng ngôn ngữ lập trình động, việc hiện thực hóa Command thường đơn giản thông qua closure - cơ chế tự động đóng gói tham số và quy trình xử lý. Ngược lại, trong C++ ta bắt buộc phải xây dựng một lớp riêng biệt. Điều này không chỉ làm tăng lượng mã nguồn mà còn gây khó khăn do lớp Command thường gắn bó mật thiết với các đối tượng xử lý lệnh. Nếu muốn hạn chế phương thức công khai, giải pháp thông dụng là sử dụng quan hệ friend với các lớp Command liên quan. Tuy nhiên, dù áp dụng cách nào đi nữa, người lập trình vẫn phải đối mặt với mã nguồn dài dòng hoặc các macro kém thẩm mỹ.
Vấn đề trở nên phức tạp hơn do C++ thiếu cơ chế thu gom rác tự động. Khi triển khai chức năng undo, việc giải phóng bộ nhớ cần được thực hiện cực kỳ thận trọng. Ví dụ, ngay cả khi xóa một loạt đối tượng, ta cũng không được phép giải phóng hoàn toàn chúng khỏi bộ nhớ mà phải giữ lại trong đối tượng Command. Một ví dụ điển hình là thao tác xóa nút trên cấu trúc TreeView - chỉ nên thực hiện thao tác “ngắt liên kết” (unlink) thay vì “xóa thật sự”, nếu không sẽ không thể khôi phục lại trạng thái trước đó.
Giải pháp thay thế là bắt đầu từ đầu chuỗi lệnh rồi thực thi lại tất cả các lệnh theo thứ tự. Tuy nhiên phương pháp này hiệu suất cực thấp và không phải lúc nào cũng khả thi, đặc biệt khi các lệnh liên quan đến dữ liệu ngoại vi. Vì không thể đảm bảo tính toàn vẹn của dữ liệu lưu trữ ngoài, việc hoàn tác có thể dẫn đến trạng thái không nhất quán.
Để giải quyết vấn đề này, lập trình viên thường áp dụng giải pháp trung hòa bằng con trỏ thông minh. Giải pháp hiệu quả hơn cả là để các đối tượng Command đảm nhận toàn quyền quản lý vòng đời của các đối tượng dữ liệu, trong khi đối tượng Document chỉ lưu trữ mối quan hệ giữa các thành phần thông qua con trỏ. So với các ngôn ngữ động hỗ trợ cả cơ chế thu gom rác và closure, cách triển khai này vẫn kém gọn gàng hơn nhiều.
Một ví dụ minh họa cụ thể là ứng dụng soạn thảo văn bản. Giả sử người dùng thực hiện thao tác gõ phím “Hello”, sau đó chọn “Ctrl+Z” để hoàn tác. Đối tượng Command tương ứng sẽ lưu trữ trạng thái trước khi thay đổi (ví dụ: vị trí con trỏ, nội dung văn bản gốc), và khi cần undo, nó đơn giản gọi phương thức restore() để đưa hệ thống về trạng thái mong muốn. Trong môi trường C++, việc quản lý thủ công các đối tượng này đòi hỏi sự cẩn trọng cao độ để tránh rò rỉ bộ nhớ hoặc truy cập vùng nhớ đã giải phóng.