Command
Khái Niệm
Command trong phát triển phần mềm, đóng vai trò như một mẫu thiết kế hành vi, giúp chuyển đổi một yêu cầu thành một đối tượng độc lập chứa đầy đủ thông tin về yêu cầu đó. Cách tiếp cận này tạo ra một cấu trúc cho phép các yêu cầu được gửi, xử lý, và lưu trữ dưới dạng các đối tượng Command. Điều này cung cấp sự linh hoạt cao trong việc quản lý yêu cầu, cũng như trong việc thực thi các hành động phức tạp.
Tổng quan
Định Nghĩa của Pattern: Command Pattern cho phép đóng gói thông tin cần thiết để thực hiện một hành động hoặc kích hoạt một sự kiện vào trong một đối tượng đơn lẻ. Điều này bao gồm thông tin về phương thức nào được gọi, người gọi, và các tham số của phương thức.
Mục Đích: Mục đích chính của mẫu thiết kế này là tách rời người phát ra yêu cầu (sender) khỏi đối tượng thực hiện yêu cầu (receiver). Điều này giúp giảm sự phụ thuộc giữa các lớp gọi và lớp nhận, từ đó tăng tính mô-đun và khả năng mở rộng của ứng dụng.
Ý Tưởng Cốt Lõi: Trong Command Pattern, mỗi yêu cầu hoặc hành động được đóng gói thành một đối tượng Command riêng biệt. Điều này cho phép lưu trữ, xếp hàng, và kiểm soát các yêu cầu một cách linh hoạt. Nó cũng hỗ trợ hoạt động hoãn hành, undo/redo, hoặc ghi nhật ký các thao tác.
Đặt vấn đề
Giả sử bạn đang làm việc trên một ứng dụng soạn thảo văn bản mới. Công việc hiện tại của bạn là tạo một thanh công cụ (toolbar) với vài nút (button) để chỉnh sửa. Bạn bắt đầu bằng cách tạo một lớp cơ sở Button
để sử dụng cho các nút trên toolbar.
Các nút này có vẻ rất giống nhau, nhưng sự kiện xử lý của mỗi nút lại khác biệt. Làm sao để gán sự kiện cho mỗi nút button? Cách đơn giản nhất là tạo các lớp riêng biệt cho từng sự kiện và kế thừa lớp cơ sở Button
.
Sau một thời gian, bạn nhận ra rằng phương pháp này có nhiều hạn chế. Đầu tiên, sự gia tăng của lớp con khiến kiến trúc trở nên phức tạp. Hơn nữa, mỗi khi lớp cơ sở Button
thay đổi, việc bảo trì mã nguồn trở nên khó khăn.
Vấn đề trở nên nghiêm trọng hơn khi ứng dụng phát triển: người dùng không chỉ muốn thao tác thông qua các nút mà còn muốn sử dụng phím tắt hoặc các thao tác kéo thả. Điều này đòi hỏi bạn phải tạo thêm lớp để xử lý sự kiện từ phím tắt, và sau đó, phải sao chép toàn bộ chức năng đã được triển khai trong các nút button. Điều này không những tốn công mà còn gây khó khăn khi cần cập nhật chức năng, vì bạn phải thực hiện thay đổi ở nhiều nơi.
Giải pháp
Để giải quyết vấn đề này, chúng ta cần phân tách giao diện người dùng và logic nền tảng thành hai lớp riêng biệt. Lớp GUI (Giao Diện Người Dùng) chịu trách nhiệm hiển thị giao diện trực quan và thân thiện cho người dùng, đồng thời lắng nghe và phản ứng với các tương tác từ phía người dùng. Tuy nhiên, khi đến phần xử lý tính toán, lưu trữ, và truy vấn dữ liệu, thì lớp GUI nên chuyển giao nhiệm vụ này cho lớp logic.
Có thể bạn sẽ nghĩ chỉ cần tạo ra một lớp logic để nhận và xử lý các sự kiện từ nút Button
là đủ. Tuy nhiên, theo Command Pattern, tôi khuyến nghị không nên để GUI gọi trực tiếp các yêu cầu. Thay vào đó, bạn nên tạo một lớp đặc biệt, trong đó chứa thông tin và yêu cầu cần thiết, cùng với tên phương thức sẽ được gọi và một phương thức để kích hoạt yêu cầu đó.
Nhờ cách làm này, sự liên kết chặt chẽ giữa GUI và Logic sẽ được giảm bớt, giúp bạn có thêm sự linh hoạt trong quá trình sử dụng và phát triển phần mềm.
Cấu Trúc
Command (Interface):
Đây là một interface hoặc abstract class định nghĩa phương thức
execute()
.Mục đích là để tạo một giao diện chung cho tất cả các lệnh cụ thể.
ConcreteCommand:
Là một lớp cụ thể thực hiện interface
Command
.Trong phương thức
execute()
, nó gọi phương thức tương ứng của Receiver.
Invoker:
Lưu trữ một tham chiếu đến một đối tượng
Command
.Gọi phương thức
execute()
trên đối tượngCommand
để thực hiện yêu cầu.
Receiver:
Biết cách thực hiện các hoạt động cần thiết để thực hiện yêu cầu.
Mỗi
ConcreteCommand
sẽ liên kết với mộtReceiver
.
Client:
Tạo ra một đối tượng
ConcreteCommand
và thiết lập receiver của nó.Có thể giao
Command
choInvoker
để thực hiện.
Cách triển khai
1. Command Interface
Đầu tiên, chúng ta cần định nghĩa interface Command
với phương thức execute()
.
2. ConcreteCommand
Tiếp theo, tạo các lớp ConcreteCommand
thực thi interface Command
. Mỗi ConcreteCommand
sẽ chứa một tham chiếu đến Receiver
và gọi phương thức của Receiver
trong execute()
.
3. Receiver
Lớp Receiver
biết cách thực hiện các hoạt động thực sự.
4. Invoker
Invoker
lưu trữ một tham chiếu đến Command
và gọi execute()
trên nó.
5. Client
Cuối cùng, Client
tạo ConcreteCommand
và thiết lập Receiver
của nó. Nó cũng có thể giao Command
cho Invoker
.
Ví dụ áp dụng Command Pattern
Trong ví dụ này, mẫu Command Pattern được triển khai để mô phỏng hoạt động của một ứng dụng ngân hàng quản lý các thao tác liên quan đến tài khoản như mở và đóng tài khoản. Mẫu này tách biệt đối tượng gọi thao tác (lớp BankApp
) với đối tượng biết cách thực hiện nó (lớp Account
). Dưới đây là phần phân tích các thành phần:
Tài khoản (Account.kt): Lớp này đại diện cho một tài khoản ngân hàng với các chức năng cơ bản để mở và đóng tài khoản. Các phương thức
open()
vàclose()
in ra các thông báo chỉ ra trạng thái của tài khoản.Lệnh (Command.kt): Đây là một giao diện với một phương thức duy nhất
execute()
. Nó là trung tâm của Command Pattern, cho phép các lớp lệnh cụ thể triển khai phương thức này.Mở Tài Khoản (OpenAccount.kt): Một lớp lệnh cụ thể triển khai giao diện
Command
. Nó đóng gói hành độngopen()
của mộtAccount
.Đóng Tài Khoản (CloseAccount.kt): Một lớp lệnh cụ thể khác triển khai giao diện
Command
. Nó đóng gói hành độngclose()
của mộtAccount
.Ứng Dụng Ngân Hàng (BankApp.kt): Lớp này đóng vai trò như một người gọi. Nó có các phương thức (
clickOpenAccount()
vàclickCloseAccount()
) thực thi các lệnh tương ứng.Chính (main.kt): Điểm nhập của chương trình, nơi một
Account
được tạo, các đối tượng lệnh để mở và đóng tài khoản được khởi tạo, vàBankApp
được sử dụng để thực thi các lệnh này.
Account.kt
Command.kt
OpenAccount.kt
CloseAccount.kt
BankApp.kt
main.kt
Kết quả
Khi nào áp dụng
Khi bạn muốn chỉ định một hành động cụ thể cho một đối tượng: Nếu bạn cần mỗi đối tượng có thể thực hiện một hành động đặc biệt nào đó, Command Pattern sẽ giúp bạn làm điều này. Nó cho phép bạn 'đóng gói' một hành động vào một đối tượng, rồi sau đó bạn có thể sử dụng hành động đó mọi lúc mọi nơi.
Khi bạn muốn thực hiện hành động vào lúc khác: Đôi khi bạn không muốn thực hiện một hành động ngay lập tức. Ví dụ, bạn có thể muốn lên lịch nó cho sau này hoặc chờ đến khi điều kiện nào đó được đáp ứng. Command Pattern giúp bạn lưu trữ hành động đó và thực hiện nó vào thời điểm bạn chọn.
Khi bạn cần tính năng 'hoàn tác': Trong trường hợp bạn muốn người dùng có thể 'hoàn tác' một hành động nào đó mà họ đã thực hiện, Command Pattern có thể giúp. Nó cho phép bạn lưu lại những gì đã xảy ra, để sau đó bạn có thể quay ngược lại nếu cần.
Khi bạn muốn phân tách người ra lệnh và người thực hiện: Nếu bạn muốn giữ cho phần của chương trình ra lệnh không phụ thuộc vào phần thực hiện lệnh, Command Pattern là một giải pháp tốt. Điều này giúp cho chương trình của bạn linh hoạt và dễ quản lý hơn.