Command Pattern (Mẫu Command)
Định nghĩa (Definition)
Command đóng gói một request thành object, cho phép parameterize client với các request khác nhau, queue hoặc log request, và hỗ trợ thao tác có thể undo (Undoable Operations).
Ví dụ Coffee Shop
Barista nhận đơn cà phê dưới dạng command object. Mỗi command có thể execute (pha cà phê), undo (hủy đơn), và lưu trong history cho undo/redo. Đơn cũng có thể được xếp hàng (Queue) trong giờ cao điểm.
Cấu trúc (Structure)
Command Interface
public interface ICommand
{
void Execute();
void Undo();
string GetDescription();
}
Receiver — Quán cà phê (Coffee Shop)
public record Beverage(string Coffee, CupSize Size, decimal Price)
{
public List<string> Toppings { get; init; } = new();
public override string ToString()
{
var base_ = $"{Size} {Coffee} — ${Price:F2}";
return Toppings.Count > 0 ? $"{base_} + [{string.Join(", ", Toppings)}]" : base_;
}
}
public class CoffeeShop
{
private readonly List<Beverage> _completedOrders = new();
public Beverage MakeCoffee(string coffee, CupSize size, decimal price)
{
var beverage = new Beverage(coffee, size, price);
_completedOrders.Add(beverage);
Console.WriteLine($" ☕ Đã pha: {beverage}");
return beverage;
}
public void CancelOrder(Beverage beverage)
{
_completedOrders.Remove(beverage);
Console.WriteLine($" ❌ Đã hủy: {beverage.Coffee}");
}
public void AddTopping(Beverage beverage, string topping)
{
beverage.Toppings.Add(topping);
Console.WriteLine($" ➕ Đã thêm {topping} vào {beverage.Coffee}");
}
public void RemoveTopping(Beverage beverage, string topping)
{
beverage.Toppings.Remove(topping);
Console.WriteLine($" ➖ Đã bỏ {topping} khỏi {beverage.Coffee}");
}
}
Concrete Command
public class OrderCoffeeCommand : ICommand
{
private readonly CoffeeShop _shop;
private readonly string _coffee;
private readonly CupSize _size;
private readonly decimal _price;
private Beverage? _lastBeverage;
public OrderCoffeeCommand(CoffeeShop shop, string coffee, CupSize size, decimal price)
{
_shop = shop;
_coffee = coffee;
_size = size;
_price = price;
}
public void Execute()
{
_lastBeverage = _shop.MakeCoffee(_coffee, _size, _price);
}
public void Undo()
{
if (_lastBeverage is not null)
_shop.CancelOrder(_lastBeverage);
}
public string GetDescription() => $"Đặt {_size} {_coffee}";
}
public class AddToppingCommand : ICommand
{
private readonly CoffeeShop _shop;
private readonly Beverage _beverage;
private readonly string _topping;
public AddToppingCommand(CoffeeShop shop, Beverage beverage, string topping)
{
_shop = shop;
_beverage = beverage;
_topping = topping;
}
public void Execute() => _shop.AddTopping(_beverage, _topping);
public void Undo() => _shop.RemoveTopping(_beverage, _topping);
public string GetDescription() => $"Thêm {_topping}";
}
Invoker — Barista (với Undo/Redo)
public class Barista
{
private readonly Stack<ICommand> _history = new();
private readonly Stack<ICommand> _redoStack = new();
public void ExecuteCommand(ICommand command)
{
command.Execute();
_history.Push(command);
_redoStack.Clear(); // Command mới xóa redo history
}
public void Undo()
{
if (_history.Count == 0) return;
var command = _history.Pop();
command.Undo();
_redoStack.Push(command);
Console.WriteLine($" ↩️ Undo: {command.GetDescription()}");
}
public void Redo()
{
if (_redoStack.Count == 0) return;
var command = _redoStack.Pop();
command.Execute();
_history.Push(command);
Console.WriteLine($" ↪️ Redo: {command.GetDescription()}");
}
}
Client
var shop = new CoffeeShop();
var barista = new Barista();
barista.ExecuteCommand(new OrderCoffeeCommand(shop, "Latte", CupSize.Large, 5.00m));
barista.ExecuteCommand(new OrderCoffeeCommand(shop, "Espresso", CupSize.Small, 2.50m));
// Undo đơn Espresso
barista.Undo();
// ❌ Đã hủy: Espresso / ↩️ Undo: Đặt Small Espresso
// Redo — lấy lại Espresso
barista.Redo();
// ☕ Đã pha: Small Espresso / ↪️ Redo: Đặt Small Espresso
Mỗi action là command object — nó capture request, receiver, và biết cách tự undo. Điều này cho phép undo/redo, command queue, và logging mà không cần sửa receiver.
Sử dụng thực tế trong .NET (.NET Real-World Usage)
IAsyncResult/ Task — command như thao tác bất đồng bộ- WPF
ICommand— MVVM command binding cho button - ASP.NET Core middleware — mỗi middleware là command trong pipeline
- CQRS — tách biệt command và query
System.Transaction— rollback qua compensating action
Khi nào sử dụng (When to Use)
- Cần undo/redo thao tác
- Muốn queue, schedule, hoặc log request
- Cần parameterize object với thao tác
- Muốn implement macro command (batch operation)
Khi nào KHÔNG sử dụng (When NOT to Use)
- Thao tác đơn giản và không cần undo — gọi method trực tiếp
- Không cần queue, log, hoặc undo
- Overhead của command object không đáng
Điểm chính (Key Takeaways)
- Command đóng gói request thành object với
Execute()vàUndo() - Tách biệt invoker (barista) khỏi receiver (coffee shop)
- Cho phép undo/redo, queue, log, và macro operation
- Mỗi command tự chứa — biết receiver và action
Câu hỏi phỏng vấn (Interview Questions)
Q: Command cho phép undo/redo như thế nào?
Mỗi command lưu state cần thiết để đảo ngược hiệu ứng. Undo() áp dụng thao tác nghịch. Invoker duy trì stack — pop để undo, push vào redo stack, pop từ redo stack để redo.
Q: Command khác Strategy như thế nào? Command đóng gói request (hành động cần thực hiện). Strategy đóng gói thuật toán (cách tính toán). Command thường là one-shot và undoable; strategy là long-lived và swappable.
Q: CQRS là gì? Command Query Responsibility Segregation tách read operation (query) khỏi write operation (command). Command thay đổi state và không trả về gì. Query trả về data và không thay đổi state.
Chủ đề liên quan (Related Topics)
- Strategy — đóng gói thuật toán, không phải request
- Chain of Responsibility — chuyển request dọc theo chuỗi
- SOLID — SRP — mỗi command có một trách nhiệm duy nhất