Command Pattern trong Ruby

Mục đích của Command pattern

Đầu tiên, việc hình thành ra các design patterns là một "phát minh" lớn đối với các developer, bởi nó cung cấp chuẩn hóa cho việc giải quyết các vấn đề. Và như mọi người cũng có đọc qua thì quyển sách Gang of four là quyển sách đầu tiên đưa ra các khái niệm về design patterns. Hôm nay mình sẽ cùng tìm hiểu với mọi người về một pattern khác đó chính là Command Pattern, thực ra mình đọc và dịch lại nhiều hơn.

Theo Gof (Gang of four) mục đích của Command pattern là: "Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations."
Mọi người có thể dịch đại khái như mình là các request (yêu cầu) từ client (máy khách) gửi lên sẽ được đóng gói thành đối tượng. Từ đó giúp bạn tham số hóa các request tới từ các client khác nhau. Theo kiểu có thể đẩy vào hàng đợi, ghi lại lịch sử các request hoặc là hỗ trợ cho các thao tác không thể phục hồi, ...

Các ví dụ và lợi ích của Command pattern

Mình sẽ lấy lại ví dụ mình đọc được vì nó khá hay và dễ hiểu

Hãy tưởng tượng chúng ta đang có chuyến đi du lịch ở Hawaii và đã đặt phòng ở khách sạn Luxury
Chúng ta đã dành cả ngày trên bãi biển, lặn biển và tham quan một số vùng biển. Đã đến lúc trở lại khách sạn để thư giãn, ăn cơm và lên kế hoạch cho ngày tiếp theo.
Sau khi trở lại khách sạn, chúng ta muốn:

  1. Gọi dịch vụ phục vụ phòng cho bữa tối.
  2. Gọi dịch vụ giặt là bởi chúng ta không mang thêm nhiều quần áo.
  3. Gọi dịch vụ hướng dẫn cho quần đảo du lịch mà ngày mai sẽ khởi hành.

Chúng ta kiểm tra menu dịch vụ của khách sạn và tìm thấy ba dịch vụ phù hợp với nhu cầu đó.
Sau đó chúng ta gọi cho bàn làm việc trước để đặt ba yêu cầu này. Một nhân viên trợ giúp sẽ gọi điện thoại cho chúng ta, ghi lại danh sách yêu cầu của chúng ta và giúp chúng ta đặt từng yêu cầu dịch vụ theo hướng dẫn của menu dịch vụ.
Sau đó mỗi nhân viên thực hiện theo từng yêu cầu cụ thể:

  1. Đầu bếp trong bếp bắt đầu nấu.
  2. Phòng vệ sinh gửi nhân viên đến phòng để lấy quần áo của chúng ta.
  3. Nhân viên trong sảnh đợi gọi một hướng dẫn viên du lịch và dẫn người đó đến phòng của chúng ta.

Rồi, giờ hãy xem lại chúng ta đã làm những gì:

  • Chúng ta đã chọn các dịch vụ mà chúng ta muốn và gửi cho nhân viên trợ giúp.
  • Nhân viên trợ giúp đã viết những yêu cầu dịch vụ này dưới dạng một danh sách.
  • Sau khi chúng ta đưa ra, được hướng dẫn bởi các menu dịch vụ, nhân viên trợ giúp đã gửi yêu cầu của chúng ta đến các phòng ban tương ứng.
  • Mỗi bộ phận thực hiện theo yêu cầu nhất định.

Đến đây, chúng ta có thể đưa các ví dụ trên xem nó sẽ tương tự thế nào trong ruby nhé:

  1. Đầu tiên, chúng ta đã gửi ba request này đến người trợ giúp (Concierge):
we.submit_request_to(concierge, 'dinner_room_service')
we.submit_request_to(concierge, 'laundry_service')
we.submit_request_to(concierge, 'travel_guide')
  1. Những request này được nhân viên trợ giúp lên danh sách theo dõi:
class Concierge
  attr_reader :request_list
  
  def initialize
    @request_list = []
  end
end

class We
  def submit_request_to(concierge, request)
    concierge.request_list << request
  end
end

Nào cùng nhìn trong console nhé:


Như chúng ta có thể thấy, sau khi we gửi ba request, những request này nằm trong request_list của concierge. 3. Được hướng dẫn bởi service menu, concierge đã gửi request của chúng ta tới các phòng ban tương ứng:

class Concierge
  attr_reader :request_list
  
  def initialize
    @request_list = []
  end
  
  def act_on_requests
    @request_list.each do |request|
      case request[:service]
        when 'room_service'
          Kitchen.execute(request[:data])
        when 'laundry_service'
          CleaningDepartment.execute(request[:data])
        when 'trip_planning_service'
          TripAdvisor.execute(request[:data])
        else
          raise 'Request Not Supported'
    end
  end
end


OK, đoạn code trên có thể chạy khá ổn ngoại trừ nó khá "smell": Cụ thể, phần mà chúng ta có các trường hợp chuyển đổi:

Tại sao phần này lại "smell"?

  1. Nếu khách sạn cung cấp 20 dịch vụ, thay vì ba, method sẽ thực sự dài.
  2. Chúng ta muốn cung cấp dịch vụ mới hoặc xóa dịch vụ hiện có. Tuy nhiên, mỗi lần làm vậy chúng ta phải mở lớp Concierge và xác định lại method act_on_request.

Do đó đã đến lúc chúng ta cần refactor chúng, hãy xem xét kỹ hơn và cùng tìm hiểu về nó: Hãy xem nó thực hiện thế nào nhé: Chúng ta lặp đi lặp lại các yêu cầu trên request_list. Đối với mỗi request, tùy theo loại dịch vụ mà chúng ta cung cấp cho các bộ phận tương ứng dữ liệu và thực hiện request cho phù hợp với từng trường hợp (đương nhiên chúng ta sẽ viết code xử lý với mỗi case). Vậy, sẽ thế nào nếu mỗi request biết bản thân request đó phải làm những gì:
Lúc đó, đoạn code cơ bản sẽ có dạng:

Thay vì để cho method act_on_request quyết định cách xử lý từng yêu cầu, chúng tôi sẽ chia sẻ trách nhiệm và kiến thức đó cho mỗi request và để cho nó tự quyết định cách xử lý.
Với điều đó đã được nói, request của chúng ta có thể như thế này:

class RoomService
  attr_reader :data, :kitchen
  
  def initialize(data)
    @data = data
    @kitchen = Kitchen.new
  end
  
  def execute
    kitchen.cook_for(data)
  end
end

class LaundryService
  attr_reader :data, :cleaning_dpt
  
  def initialize(data)
    @data = data
    @cleaning_dpt = CleaningDepartment.new
  end
  
  def execute
    cleaning_dpt.do_laundry_for(data)
  end
end

class TripPlanningService
  attr_reader :data, :tripAdvisor
  
  def initialize(data)
    @data = data
    @tripAdvisor = TripAdvisor.new
  end
  
  def execute
    tripAdvisor.plan_for(data)
  end
end


Và lúc đó Concierge sẽ được sửa lại như sau:

class Concierge
  attr_reader :request_list
  
  def initialize
    @request_list = []
  end
  
  def act_on_requests
    @request_list.each do |request|
      request.execute
    end
  end
end

Với đoạn code mới, đây là cách chúng ta, khách hàng của khách sạn, gửi yêu cầu tới người trợ giúp.

Nó khá dễ dàng để tạo ra một dịch vụ khác.
Ví dụ, khách sạn cũng cho phép chúng ta sử dụng SPA:

class SpaReservationService
  attr_reader :data, :spa_center
  
  def initialize(data)
    @data = data
    @spa_center = SpaCenter.new
  end
  
  def execute
    spa_center.reserve(data)
  end
  
  def undo
    spa_center.cancel(data)
  end
end

Dịch vụ này không chỉ hỗ trợ execute (đặt phòng spa) mà còn phải undo (huỷ đặt phòng).

Giả sử khách sạn cũng cung cấp một cách khác để yêu cầu dịch vụ mà không cần phải gọi cho nhân viên trợ giúp - một bảng yêu cầu dịch vụ:
Chúng ta chỉ cần nhấn nút và dịch vụ với cài đặt mặc định sẽ được gửi đến phòng của điều hành.

class ServicePanel
  attr_reader :button_a1, :button_a2, :button_b1, :button_b2
  
  def initialize(a1_service, a2_service, 
    b1_service, b2_service)
    @button_a1 = Button.new(a1_service)
    @button_a2 = Button.new(a2_service)
    @button_b1 = Button.new(b1_service)
    @button_b2 = Button.new(b2_service)
  end
end

class Button
  attr_reader :service
  
  def initialize(service)
    @service = service
  end
  
  def on_button_click
    service.execute
  end
end


Và đây là cách chúng ta có thể tạo bảng điều khiển dịch vụ:

Kết luận

Trên đây là phần tìm hiểu của mình và nó khá dài, hãy cùng đi lại từng phần nhé

  1. Đóng gói một request như một đối tượng: Mỗi class service chúng ta tạo ra, RoomService, LaundryService, TripPlanningService, và SpaReservationService chính là ví dụ của việc đóng gói đối tượng các request.
  2. Tham số hóa các request tới từ các client khác nhau: ServicePanel là một ví dụ về tham số hóa một đối tượng với các yêu cầu khác nhau.
  3. Yêu cầu hàng đợi hoặc đăng nhập và việc trợ giúp các tác vụ undo chính là phần sau mình đã nói

Các phần tìm hiểu của mình còn khá sơ sài, mong các bạn góp ý để nó hữu ích hơn nhé.

Xin cảm ơn các bạn đã theo dõi bài viết của mình.