Command Design Pattern trong Ruby

Tiếp nối cho chuổi Design Pattern trước thì hôm nay mình sẽ giới thiệu đến mọi người một Design Pattern thuộc loại Behavioural pattern đó là Command Pattern.

Command Pattern là gì?

Command Pattern là một behavioural pattern, Nó được sử dụng để đóng gói tất cả thông tin cần để thực hiện một action hay trigger một event tại một thời điểm nào sau đó. Các thông tin này bao gồm Tên method, Object sở hữu các method vào các parameters của methods.

Mục đich:

  • Đóng gói 1 request vào trong một object.
  • Tham số hóa các request từ các clients khác nhau.
  • Cho phép saving các requests vào một hàng đợi.

Khi nào thì nên dùng:

  • Tham chiếu đến một Object.
  • Xác định và thực hiện những Request tại những thời điểm khác nhau.
  • Cần thực hiện thao tác Undo
  • Cần thực hiện thao tác Logging changes (Trong trường hợp hệ thống bị treo)
  • Cấu trúc hệ thống có dạng: điều khiển cấp cao được xây dựng trên những điều khiển nền tảng.

Cụ thể nó là thế nào:

Các thành phần được dùng với command pattern:

  • Command: Định nghĩa 1 interface để thực hiện một hoạt động. Là đối tượng lưu giữ Request và State của một đối tượng tại một thời điểm.
  • ConcreteCommand:
    • Định nghĩa một kết dính giữa Receiver và một action.
    • Implements Execute bằng cách khai báo các hoạt động tương ứng trên Receiver.
  • Receiver: Là đối tượng thực hiện lệnh trên mỗi Request.
  • Invoker: Là nơi lưu trữphát sinh mỗi Request dưới dạng Command Object. Quyết định khi nào thực hiện nó.
  • Client: Tạo một ConcreteCommand object và thiết lập reveiver của nó.

Sơ đồ UML

Ưu điểm và hạn chế của nó:

  • Command Pattern tách riêng đối tượng và các request của đối tượng mà vẫn biết cách đáp ứng các request lên đối tượng đó.
  • Các Command là những Object cơ bản, chúng ta có thể vận dụng và mở rộng như bất kỳ đối tượng khác.
  • Các Command có thể được tập hợp thành một Composite Command.
  • Dễ dàng thêm một Command mới mà không cần chỉnh sửa lại các class đã có.

Example Code

#invoker
class ButtonInvoker
  def initialize(name, command)
    @name, @command = name, command
  end

  def press
    History.add @command
    @command.execute
  end
end

class History
  class << self
    def stack
      @stack ||= []
    end

    def add command
      stack << command
    end

    def undo
      command = stack.pop
      command.undo
      p "Undoing #{command.class} command"
    end

    def undo_all
      stack.each do |command|
        command.undo
        p "Undoing #{command.class} command"
      end
    end
  end
end

#Command interface
class Command
  attr_accessor :receiver

  def initialize receiver
    @receiver = receiver
  end

  def execute
    raise NotImplementedError
  end

  def undo
    raise NotImplementedError
  end
end

#ConcreteCommand
class MoveDownCommand < Command
  def execute
    receiver.move_down
  end

  def undo
    receiver.move_up
  end
end

class MoveLeftCommand < Command
  def execute
    receiver.move_left
  end

  def undo
    receiver.move_right
  end
end

class MoveRightCommand < Command
  def execute
    receiver.move_right
  end

  def undo
    receiver.move_left
  end
end

class MoveUpCommand < Command
  def execute
    receiver.move_up
  end

  def undo
    receiver.move_down
  end
end

#Receiver
class RobotReceiver
  def initialize(x = 0, y = 0)
    @x, @y = x, y
  end

  def location
    "x[#{@x}], y[#{@y}]"
  end

  def move_right
    @x += 1
  end

  def move_left
    @x -= 1
  end

  def move_up
    @y += 1
  end

  def move_down
    @y -= 1
  end
end

#Client
class Client
  attr_accessor :receiver

  def initialize
    @receiver = RobotReceiver.new
    @move_up_cmd = MoveUpCommand.new @receiver
    @move_down_cmd = MoveDownCommand.new @receiver
    @move_right_cmd = MoveRightCommand.new @receiver
    @move_left_cmd = MoveLeftCommand.new @receiver
  end

  def move_with key
    case key
    when "up"
      ButtonInvoker.new(key, @move_up_cmd).press
    when "down"
      ButtonInvoker.new(key, @move_down_cmd).press
    when "right"
      ButtonInvoker.new(key, @move_up_right).press
    else
      ButtonInvoker.new(key, @move_up_left).press
    end
  end
end

Giải thích một chút về đoạn code trên: Chúng ta có thể thấy rằng các thành phần trong Sơ đồ đều được định nghĩa đầy đủ trong đoạn code trên. RobotReceiver nó là Receiver là đó tượng thực hiện các action của mỗi lần Request. Command là được đinh nghĩa như một Interface với 2 method executeundo. Nó sẽ có các ConcreteCommand kế thừa và định nghĩa. Tạo liên kết giữa Receiver và một action. Chúng ta có 1 InvokerButtonInvoker được dùng để lưu trữ và thực hiện các Request. lựa chọn các cách request khi client yêu cầu. Và cuối cùng là Client tạo ra các ConcreteCommand và các Receiver của nó.

Các vấn đề cụ thể và Implement

Chúng ta đã hiểu pattern này hoạt động như thế, và đây là lúc chúng ta xem nó có lợi thế gì và những mặt sai sót gì cần tránh.

Sự thông minh của Command:

Có 2 điểm chú ý mà các lập trình viên phải tránh khi sử dụng pattern này:

1. Command chỉ là một liên kết giữa Receiver mà Action cái thực hiện request.
2. Command thực hiện mọi thứ của nó mà không gửi bất cứ cái gì về cho receiver.

Chúng ta phải luôn luôn giữ suy nghĩ thực tế là receiver là một người mà biết cách thực hiện một hoạt động được cần. Command giúp cho client uỷ quyền request của nó nhanh hơn và đảm bảo rằng command kết thúc nơi nó muốn.

Undo và Redo action:

Với việc sửa dụng Command Pattern bạn có thể thực hiện được việc Undo và Redo nhờ vào việc định nghĩa và lưu trữ của Invoker. Một ý tưởng tuyệt vời cho việc undo và redo tương tự cũng được áp dụng khác hay trong Memento Pattern. Bài viết sau mình sẽ chia sẽ về nó (Hy vọng có đủ nhiều thời gian)

Kết luận:

Trên đây là những điều thú vị mình tìm hiểu về Command Pattern và áp dụng nó trong Ruby. Hy vọng sẽ có ích với các bạn. Đừng ngại ngần đóng góp ý kiến hoặc chia sẽ thêm những điều thú vị về pattern này nhé. Thank all ❤️.