Command còn có tên gọi khác là Action, Transaction.
Command là một mẫu thiết kế hành vi biến một yêu cầu thành một đối tượng độc lập chứa tất cả thông tin về yêu cầu. Việc chuyển đổi này cho phép bạn tham số hóa các phương thức với các yêu cầu khác nhau, trì hoãn hoặc xếp hàng đợi việc thực hiện yêu cầu và hỗ trợ các hoạt động hoàn tác.
Hãy tưởng tượng rằng bạn đang làm việc trên một ứng dụng soạn thảo văn bản mới. Nhiệm vụ hiện tại của bạn là tạo một thanh công cụ với một loạt các nút cho các thao tác khác nhau của trình soạn thảo. Bạn đã tạo một lớp Button
rất gọn gàng có thể được sử dụng cho các nút trên thanh công cụ, cũng như cho các nút chung trong các hộp thoại khác nhau.
Mặc dù tất cả các nút này trông giống nhau, nhưng chúng đều phải làm những việc khác nhau. Bạn sẽ đặt mã xử lý sự kiện nhấp chuột của các nút này ở đâu? Giải pháp đơn giản nhất là tạo hàng tấn lớp con cho mỗi nơi mà nút được sử dụng. Các lớp con này sẽ chứa mã phải được thực thi khi nhấp vào nút.
Không lâu sau, bạn nhận ra rằng cách tiếp cận này rất thiếu sót. Đầu tiên, bạn có một số lượng lớn các lớp con, và điều đó sẽ ổn nếu bạn không mạo hiểm phá mã trong các lớp con này mỗi khi bạn sửa đổi lớp Button
cơ sở . Nói một cách đơn giản, mã GUI của bạn đã trở nên phụ thuộc một cách vụng về vào mã dễ thay đổi của logic nghiệp vụ.
Và đây là phần xấu xí nhất. Một số thao tác, chẳng hạn như Copy / Paste văn bản, sẽ cần được gọi từ nhiều nơi. Ví dụ: người dùng có thể nhấp vào nút “Copy” nhỏ trên thanh công cụ hoặc sao chép nội dung nào đó qua menu ngữ cảnh hoặc chỉ cần nhấn Ctrl+C
trên bàn phím.
Ban đầu, khi ứng dụng của chúng tôi chỉ có thanh công cụ, bạn có thể đặt việc triển khai các hoạt động khác nhau vào các lớp con của nút. Nói cách khác, có mã để sao chép văn bản bên trong lớp con CopyButton
là tốt. Nhưng sau đó, khi bạn triển khai menu ngữ cảnh, phím tắt và những thứ khác, bạn phải sao chép mã của hoạt động trong nhiều lớp hoặc làm cho menu phụ thuộc vào các nút, đó là một lựa chọn thậm chí còn tệ hơn.
Thiết kế phần mềm tốt thường dựa trên nguyên tắc tách biệt các mối quan tâm , điều này thường dẫn đến việc chia ứng dụng thành nhiều lớp. Ví dụ phổ biến nhất: một lớp cho giao diện người dùng đồ họa và một lớp khác cho logic nghiệp vụ. Lớp GUI chịu trách nhiệm hiển thị hình ảnh đẹp trên màn hình, thu nhận bất kỳ đầu vào nào và hiển thị kết quả về những gì người dùng và ứng dụng đang làm. Tuy nhiên, khi cần thực hiện một việc quan trọng, như tính toán quỹ đạo của mặt trăng hoặc soạn báo cáo hàng năm, lớp GUI sẽ ủy quyền công việc cho lớp logic nghiệp vụ cơ bản.
Trong đoạn mã, nó có thể trông như thế này: một đối tượng GUI gọi một phương thức của đối tượng logic nghiệp vụ, truyền cho nó một số đối số. Quá trình này thường được mô tả là một đối tượng gửi một yêu cầu khác.
Command pattern gợi ý rằng các đối tượng GUI không nên gửi trực tiếp các yêu cầu này. Thay vào đó, bạn nên trích xuất tất cả các chi tiết yêu cầu, chẳng hạn như đối tượng được gọi, tên của phương thức và danh sách các đối số vào một lớp command riêng biệt với một phương thức kích hoạt yêu cầu này.
Các đối tượng command đóng vai trò là liên kết giữa các đối tượng GUI và logic nghiệp vụ khác nhau. Từ bây giờ, đối tượng GUI không cần biết đối tượng logic nghiệp vụ nào sẽ nhận được yêu cầu và nó sẽ được xử lý như thế nào. Đối tượng GUI chỉ kích hoạt lệnh, lệnh này xử lý tất cả các chi tiết.
Bước tiếp theo là làm cho các lệnh của bạn triển khai cùng một interface. Thông thường nó chỉ có một phương thức thực thi duy nhất mà không cần tham số. Interface này cho phép bạn sử dụng các lệnh khác nhau với cùng một người gửi yêu cầu mà không cần kết hợp nó với các lớp lệnh cụ thể. Như một phần thưởng, bây giờ bạn có thể chuyển đổi các đối tượng lệnh được liên kết với người gửi, thay đổi hiệu quả hành vi của người gửi trong thời gian chạy.
Bạn có thể nhận thấy một phần còn thiếu của câu đố, đó là các thông số yêu cầu. Một đối tượng GUI có thể đã cung cấp cho đối tượng lớp nghiệp vụ một số tham số. Vì phương thức thực thi lệnh không có bất kỳ tham số nào, chúng ta sẽ chuyển các chi tiết yêu cầu đến người nhận như thế nào? Hóa ra command phải được cấu hình trước với dữ liệu này hoặc có thể tự lấy nó.
Hãy quay lại trình soạn thảo văn bản của chúng ta. Sau khi áp dụng Command pattern, chúng ta không cần tất cả các lớp button con đó để xử lý các sự kiện nhấp chuột. Chỉ cần đặt một trường vào lớp Button
cơ sở là đủ để lưu trữ một tham chiếu đến một đối tượng command và làm cho nút thực hiện lệnh đó khi nhấp chuột.
Bạn sẽ triển khai một loạt các lớp command cho mọi hoạt động có thể và liên kết chúng với các nút cụ thể, tùy thuộc vào hành vi dự định của các nút.
Các phần tử GUI khác, chẳng hạn như menu, phím tắt hoặc toàn bộ hộp thoại, có thể được thực hiện theo cách tương tự. Chúng sẽ được liên kết với một lệnh được thực thi khi người dùng tương tác với phần tử GUI. Như bạn có thể đoán bây giờ, các phần tử liên quan đến các hoạt động giống nhau sẽ được liên kết với các command giống nhau, ngăn chặn bất kỳ sự trùng lặp mã nào.
Kết quả là, command trở thành một lớp trung gian thuận tiện làm giảm sự phụ thuộc giữa GUI và các lớp logic nghiệp vụ. Và đó chỉ là một phần nhỏ trong những lợi ích mà command pattern có thể mang lại!
Sau một chuyến đi bộ dài trong thành phố, bạn đến một nhà hàng đẹp và ngồi vào bàn bên cửa sổ. Một người phục vụ thân thiện tiếp bạn và nhanh chóng nhận đặt món của bạn, viết nó ra một tờ giấy. Người phục vụ đi vào bếp và dán tờ giấy order lên tường. Sau một thời gian, các món bạn order được chuyển đến đầu bếp, người sẽ đọc và nấu bữa ăn cho bạn. Người đầu bếp đặt bữa ăn vào khay cùng với order của bạn. Người phục vụ nhận khay thức ăn, kiểm tra order để đảm bảo mọi thứ đều như bạn mong muốn và mang mọi thứ đến bàn của bạn.
Tờ giấy order dùng như một mệnh lệnh. Nó vẫn còn trong hàng đợi cho đến khi đầu bếp sẵn sàng phục vụ nó. Order có tất cả các thông tin liên quan cần thiết để nấu bữa ăn. Nó cho phép đầu bếp bắt đầu nấu ăn ngay lập tức thay vì chạy xung quanh để làm rõ chi tiết order trực tiếp từ bạn.
Trong ví dụ này, Command pattern giúp theo dõi lịch sử của các hoạt động đã thực thi và giúp bạn có thể hoàn tác một hoạt động nếu cần.
Các lệnh dẫn đến thay đổi trạng thái của trình soạn thảo (ví dụ: cut và paste) tạo một bản sao lưu trạng thái của trình soạn thảo trước khi thực hiện một thao tác được liên kết với lệnh. Sau khi một lệnh được thực thi, nó được đặt vào lịch sử lệnh (một ngăn xếp các đối tượng command) cùng với bản sao lưu trạng thái của trình soạn thảo tại thời điểm đó. Sau đó, nếu người dùng cần hoàn tác một thao tác, ứng dụng có thể lấy lệnh gần đây nhất từ lịch sử, đọc bản sao lưu liên quan về trạng thái của trình chỉnh sửa và khôi phục nó.
Mã client (các phần tử GUI, lịch sử lệnh, v.v.) không được kết hợp với các lớp command cụ thể vì nó hoạt động với các command thông qua interface Command. Cách tiếp cận này cho phép bạn thêm các lệnh mới vào ứng dụng mà không phá vỡ bất kỳ mã hiện có nào.
// The base command class defines the common interface for all
// concrete commands.
abstract class Command is
protected field app: Application
protected field editor: Editor
protected field backup: text
constructor Command(app: Application, editor: Editor) is
this.app = app
this.editor = editor
// Make a backup of the editor's state.
method saveBackup() is
backup = editor.text
// Restore the editor's state.
method undo() is
editor.text = backup
// The execution method is declared abstract to force all
// concrete commands to provide their own implementations.
// The method must return true or false depending on whether
// the command changes the editor's state.
abstract method execute()
// The concrete commands go here.
class CopyCommand extends Command is
// The copy command isn't saved to the history since it
// doesn't change the editor's state.
method execute() is
app.clipboard = editor.getSelection()
return false
class CutCommand extends Command is
// The cut command does change the editor's state, therefore
// it must be saved to the history. And it'll be saved as
// long as the method returns true.
method execute() is
saveBackup()
app.clipboard = editor.getSelection()
editor.deleteSelection()
return true
class PasteCommand extends Command is
method execute() is
saveBackup()
editor.replaceSelection(app.clipboard)
return true
// The undo operation is also a command.
class UndoCommand extends Command is
method execute() is
app.undo()
return false
// The global command history is just a stack.
class CommandHistory is
private field history: array of Command
// Last in...
method push(c: Command) is
// Push the command to the end of the history array.
// ...first out
method pop():Command is
// Get the most recent command from the history.
// The editor class has actual text editing operations. It plays
// the role of a receiver: all commands end up delegating
// execution to the editor's methods.
class Editor is
field text: string
method getSelection() is
// Return selected text.
method deleteSelection() is
// Delete selected text.
method replaceSelection(text) is
// Insert the clipboard's contents at the current
// position.
// The application class sets up object relations. It acts as a
// sender: when something needs to be done, it creates a command
// object and executes it.
class Application is
field clipboard: string
field editors: array of Editors
field activeEditor: Editor
field history: CommandHistory
// The code which assigns commands to UI objects may look
// like this.
method createUI() is
// ...
copy = function() { executeCommand(
new CopyCommand(this, activeEditor)) }
copyButton.setCommand(copy)
shortcuts.onKeyPress("Ctrl+C", copy)
cut = function() { executeCommand(
new CutCommand(this, activeEditor)) }
cutButton.setCommand(cut)
shortcuts.onKeyPress("Ctrl+X", cut)
paste = function() { executeCommand(
new PasteCommand(this, activeEditor)) }
pasteButton.setCommand(paste)
shortcuts.onKeyPress("Ctrl+V", paste)
undo = function() { executeCommand(
new UndoCommand(this, activeEditor)) }
undoButton.setCommand(undo)
shortcuts.onKeyPress("Ctrl+Z", undo)
// Execute a command and check whether it has to be added to
// the history.
method executeCommand(command) is
if (command.execute)
history.push(command)
// Take the most recent command from the history and run its
// undo method. Note that we don't know the class of that
// command. But we don't have to, since the command knows
// how to undo its own action.
method undo() is
command = history.pop()
if (command != null)
command.undo()
Sử dụng Command pattern khi bạn muốn tham số hóa các đối tượng bằng các phép toán.
Command pattern có thể biến một lời gọi phương thức cụ thể thành một đối tượng độc lập. Thay đổi này mở ra rất nhiều cách sử dụng thú vị: bạn có thể chuyển các lệnh làm đối số của phương thức, lưu trữ chúng bên trong các đối tượng khác, chuyển đổi các lệnh được liên kết trong thời gian chạy, v.v.
Đây là một ví dụ: bạn đang phát triển thành phần GUI chẳng hạn như menu ngữ cảnh và bạn muốn người dùng của mình có thể cấu hình các mục menu kích hoạt hoạt động khi người dùng cuối nhấp vào một phần tử.
Sử dụng Command pattern khi bạn muốn tạo hàng đợi các thao tác, lập lịch thực thi hoặc thực thi chúng từ xa.
Như với bất kỳ đối tượng nào khác, một command có thể được serialize, có nghĩa là chuyển đổi nó thành một chuỗi để có thể dễ dàng ghi vào tệp hoặc cơ sở dữ liệu. Sau đó, chuỗi có thể được deserialize thanhf đối tượng command ban đầu. Do đó, bạn có thể trì hoãn và lên lịch thực hiện command. Nhưng còn nhiều hơn thế nữa! Theo cách tương tự, bạn có thể tạo hàng đợi, ghi nhật ký hoặc gửi command qua mạng.
Sử dụng Command pattern khi bạn muốn thực hiện các hoạt động có thể đảo ngược.
Mặc dù có nhiều cách để thực hiện hoàn tác / làm lại, nhưng Command pattern có lẽ là phổ biến nhất.
Để có thể hoàn tác các hoạt động, bạn cần thực hiện lịch sử của các hoạt động đã thực hiện. Lịch sử lệnh là một ngăn xếp chứa tất cả các đối tượng command được thực thi cùng với các bản sao lưu liên quan về trạng thái của ứng dụng.
Phương pháp này có hai nhược điểm. Đầu tiên, không dễ để lưu trạng thái của ứng dụng vì một số trong số đó có thể là riêng tư. Vấn đề này có thể được giảm thiểu với mẫu Memento.
Thứ hai, các bản sao lưu trạng thái có thể tiêu tốn khá nhiều RAM. Do đó, đôi khi bạn có thể sử dụng một cách triển khai thay thế: thay vì khôi phục trạng thái trước đây, lệnh thực hiện thao tác ngược. Hoạt động ngược lại cũng có một cái giá: nó có thể khó hoặc thậm chí không thể thực hiện được.
Execute
duy nhất.Ưu điểm | Nhược điểm |
---|---|
|
|
Chain of Responsibility, Command, Mediator và Observer giải quyết các cách khác nhau để kết nối người gửi và người nhận yêu cầu:
Các trình xử lý trong Chain of Responsibility có thể được thực hiện dưới dạng Command. Trong trường hợp này, bạn có thể thực thi nhiều thao tác khác nhau trên cùng một đối tượng ngữ cảnh, được thể hiện bằng một yêu cầu.
Tuy nhiên, có một cách tiếp cận khác, trong đó bản thân yêu cầu là một đối tượng Command. Trong trường hợp này, bạn có thể thực hiện cùng một thao tác trong một loạt các ngữ cảnh khác nhau được liên kết thành một chuỗi.
Bạn có thể sử dụng Command và Memento cùng nhau khi thực hiện "hoàn tác". Trong trường hợp này, các lệnh chịu trách nhiệm thực hiện các hoạt động khác nhau trên một đối tượng đích, trong khi các mementos lưu trạng thái của đối tượng đó ngay trước khi lệnh được thực thi.
Command và Strategy có thể trông giống nhau vì bạn có thể sử dụng cả hai để tham số hóa một đối tượng bằng một số hành động. Tuy nhiên, chúng có mục đích rất khác nhau.
Prototype có thể hữu ích khi bạn cần lưu các bản sao của Command vào lịch sử.
Bạn có thể coi Visitor như một phiên bản mạnh mẽ của mẫu Command. Các đối tượng của nó có thể thực thi các hoạt động trên các đối tượng khác nhau của các lớp khác nhau.
Độ phức tạp: 2/5
Phổ biến: 5/5
Ví dụ sử dụng: Command pattern khá phổ biến trong mã C#. Thông thường, nó được sử dụng như một sự thay thế cho các lệnh gọi lại để tham số hóa các phần tử giao diện người dùng bằng các hành động. Nó cũng được sử dụng cho các tác vụ xếp hàng, theo dõi lịch sử hoạt động, v.v.
Nhận biết: Command pattern có thể nhận biết được bằng các phương thức trong abstract class/interface (người gửi). Phương thức này gọi một phương thức được triển khai trong một abstract class/interface khác (người nhận) đã được đóng gói bởi việc triển khai command trong quá trình tạo nó. Các lớp command thường được giới hạn trong các hành động cụ thể.
Ví dụ sau minh họa cấu trúc của Command pattern. Nó tập trung vào việc trả lời những câu hỏi sau:
using System;
namespace Comdy.DesignPattern.Command.Conceptual
{
// The Command interface declares a method for executing a command.
public interface ICommand
{
void Execute();
}
// Some commands can implement simple operations on their own.
class SimpleCommand : ICommand
{
private string _payload = string.Empty;
public SimpleCommand(string payload)
{
this._payload = payload;
}
public void Execute()
{
Console.WriteLine($"SimpleCommand: See, I can do simple things like printing ({this._payload})");
}
}
// However, some commands can delegate more complex operations to other
// objects, called "receivers."
class ComplexCommand : ICommand
{
private Receiver _receiver;
// Context data, required for launching the receiver's methods.
private string _a;
private string _b;
// Complex commands can accept one or several receiver objects along
// with any context data via the constructor.
public ComplexCommand(Receiver receiver, string a, string b)
{
this._receiver = receiver;
this._a = a;
this._b = b;
}
// Commands can delegate to any methods of a receiver.
public void Execute()
{
Console.WriteLine("ComplexCommand: Complex stuff should be done by a receiver object.");
this._receiver.DoSomething(this._a);
this._receiver.DoSomethingElse(this._b);
}
}
// The Receiver classes contain some important business logic. They know how
// to perform all kinds of operations, associated with carrying out a
// request. In fact, any class may serve as a Receiver.
class Receiver
{
public void DoSomething(string a)
{
Console.WriteLine($"Receiver: Working on ({a}.)");
}
public void DoSomethingElse(string b)
{
Console.WriteLine($"Receiver: Also working on ({b}.)");
}
}
// The Invoker is associated with one or several commands. It sends a
// request to the command.
class Invoker
{
private ICommand _onStart;
private ICommand _onFinish;
// Initialize commands.
public void SetOnStart(ICommand command)
{
this._onStart = command;
}
public void SetOnFinish(ICommand command)
{
this._onFinish = command;
}
// The Invoker does not depend on concrete command or receiver classes.
// The Invoker passes a request to a receiver indirectly, by executing a
// command.
public void DoSomethingImportant()
{
Console.WriteLine("Invoker: Does anybody want something done before I begin?");
if (this._onStart is ICommand)
{
this._onStart.Execute();
}
Console.WriteLine("Invoker: ...doing something really important...");
Console.WriteLine("Invoker: Does anybody want something done after I finish?");
if (this._onFinish is ICommand)
{
this._onFinish.Execute();
}
}
}
class Program
{
static void Main(string[] args)
{
// The client code can parameterize an invoker with any commands.
Invoker invoker = new Invoker();
invoker.SetOnStart(new SimpleCommand("Say Hi!"));
Receiver receiver = new Receiver();
invoker.SetOnFinish(new ComplexCommand(receiver, "Send email", "Save report"));
invoker.DoSomethingImportant();
}
}
}
Kết quả:
Invoker: Does anybody want something done before I begin?
SimpleCommand: See, I can do simple things like printing (Say Hi!)
Invoker: ...doing something really important...
Invoker: Does anybody want something done after I finish?
ComplexCommand: Complex stuff should be done by a receiver object.
Receiver: Working on (Send email.)
Receiver: Also working on (Save report.)
Visitor là một mẫu thiết kế hành vi cho phép bạn tách các thuật toán khỏi các đối tượng mà chúng hoạt động.
Template Method định nghĩa khung của một thuật toán trong lớp cha cho phép lớp con ghi đè các bước cụ thể của thuật toán mà không thay đổi cấu trúc của nó.
Strategy cho phép bạn định nghĩa một nhóm thuật toán, đặt mỗi thuật toán vào một lớp riêng biệt và làm cho các đối tượng của chúng có thể hoán đổi cho nhau.
State là một mẫu thiết kế hành vi cho phép một đối tượng thay đổi hành vi của nó khi trạng thái bên trong của nó thay đổi.