Command Pattern trong Ruby
Bài đăng này đã không được cập nhật trong 6 năm
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:
- Gọi dịch vụ phục vụ phòng cho bữa tối.
- Gọi dịch vụ giặt là bởi chúng ta không mang thêm nhiều quần áo.
- 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ể:
- Đầu bếp trong bếp bắt đầu nấu.
- Phòng vệ sinh gửi nhân viên đến phòng để lấy quần áo của chúng ta.
- 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é:
- Đầ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')
- 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"?
- Nếu khách sạn cung cấp 20 dịch vụ, thay vì ba, method sẽ thực sự dài.
- 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 methodact_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é
- Đó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. - 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. - 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.
All rights reserved