Sử dụng Command pattern trong Ruby on Rail

Mục đích của Command pattern

Có thể nói phát minh ra Design Patterns là một phát kiến vĩ đại trong giới lập trình viên. Bởi vì nó có thể chuẩn hóa các vấn đề hay mắc phải. Cũng như khi các bạn đọc qua tài liệu gối đầu giường Gang of four ( kẻ khai sáng khái niệm design patterns ). Hôm nay chúng ta sẽ tìm hiểu về Command Pattern trong Ruby on Rail.

Cách hiểu Command Pattern đại khái là: các request từ client lên sẽ được đóng gói thành đốit ượng hết. Từ đó bạn có thể sử dụng request như một tham số tới các client khác nhau. Mà nếu request là đối tượng nghĩa là bạn có thể cho nó vào hàng chờ, ghi lại request history hoặc có thể khôi phục các thao tác request cũ…

Công dụng chính của Command Pattern

Hãy tưởng tượng rằng ta đang cần đặt phòng ở 1 khách sạn tên Luxury và chúng ta muốn có các request:

  1. Gọi bữa tối.
  2. Gọi dịch vụ giặt ủi.
  3. Gọi một guider cho ngày mai đi tham quan.

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

  1. Đóng gói một request như một đối tượng: Mỗi class service chúng ta tạo ra, RoomServiceLaundryServiceTripPlanningService, 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

Bài viết được tham khảo từ bạn: Tất Đạt ở Viblo.