Chuyển tới nội dung chính

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ẹo

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()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.