Chain of Responsibility Pattern - Ruby
Chain of Responsibility là một mẫu thiết kế giải quyết cho việc thực hiện 1 chuỗi các tác vụ có trình tự mà mỗi 1 tác vụ trong chuỗi đó được đảm nhiệm bởi 1 class. Định nghĩa này khá dễ hiểu so với các định nghĩa hàn lâm khác về Chain of Responsibility Pattern, chúng ta sẽ đi từ ví dụ để hiểu ...
Chain of Responsibility là một mẫu thiết kế giải quyết cho việc thực hiện 1 chuỗi các tác vụ có trình tự mà mỗi 1 tác vụ trong chuỗi đó được đảm nhiệm bởi 1 class.
Định nghĩa này khá dễ hiểu so với các định nghĩa hàn lâm khác về Chain of Responsibility Pattern, chúng ta sẽ đi từ ví dụ để hiểu rõ hơn pattern này.
Ví dụ 1
Giả sử tôi có một ứng dụng thanh toán tiền cho khách hàng, tuỳ vào số tiền và đơn vị tiền tệ, tôi muốn sử dụng các nhà cung cấp dịch vụ thanh toán khác nhau để xử lý khoản tiền đó .
Để xác định nhà cung cấp cho mỗi giao dịch cụ thể, tôi có đoạn code với điều kiện như sau:
if ...some logic for transaction use payment provider 1 elsif ...logic use payment provider 2 elsif ...logic use payment provider 3 end
Nếu logic phức tạp, viết code như vậy rất rườm rà và khó refactor.
Chain of Responsibility cho phép xây dựng một chuỗi các xử lý. Mỗi trình xử lý sẽ chứa logic để xử lý một loại giao dịch.
Một giao dịch sẽ đi qua chuỗi đó cho đến khi gặp một trình xử lý phù hợp. Có thể hình dung nó như thế này:
Mỗi trình xử lý phải chứa logic để quyết định xem nó có xử lý được giao dịch đó ko, nếu không sẽ chạy đến trình xử lý tiếp theo trong chuỗi.
Với chuỗi này, đầu tiên, Handler#1 sẽ cố gắng xử lý giao dịch. Nếu nó không xử lý được, sẽ chạy Handler#2. Nếu Handler#2 cũng không xử lý được, sẽ chạy Handler#3.
Lợi ích của cách tiếp cận này:
- Có thể xác định một trình tự xử lý
- Mỗi trình xử lý sẽ chứa logic riêng
- Dễ dàng thêm trình xử lý mới
- Có thể đi từ các trình xử lý cụ thể đến các trình xử lý chung
Nào cùng sử dụng Chain of Responsibility cho ví dụ này.
Trước hết, tạo ra một lớp đơn giản cho một giao dịch:
class Transaction attr_reader :amount, :currency def initialize(amount, currency) @amount = amount @currency = currency end end
Tiếp theo, xác định logic cho trình xử lý giao dịch. Nếu thoả mãn can_handle? sẽ gọi phương thức handle để xử lý giao dịch. Nếu không, sẽ gọi trình xử lý tiếp theo. Ở đây, tôi sẽ gọi trình xử lý kế tiếp trong một chuỗi successor. Tôi đặt logic này vào lớp cơ sở. Mỗi trình xử lý sẽ được kế thừa từ lớp này:
class BaseHandler attr_reader :successor def initialize(successor = nil) @successor = successor end def call(transaction) return successor.call(transaction) unless can_handle?(transaction) handle(transaction) end def handle(_transaction) raise NotImplementedError, 'Each handler should respond to handle and can_handle? methods' end end
Cùng đi vào chi tiết.
def initialize(successor = nil) @successor = successor end
Khởi tạo chuỗi successor.
chain = StripeHandler.new(BraintreeHandler.new) chain.call(transaction)
Sử dụng phương thức call(transaction).
def call(transaction) return successor.call(transaction) unless can_handle?(transaction) handle(transaction) end
Khi dùng call(transaction) trong trình xử lý đầu tiên, sẽ kiểm tra xem nó có thể xử lý giao dịch không, nếu không, gọi successor.call(transaction) và truyền luồng đến trình xử lý kế tiếp trong chuỗi.
Do đó, mỗi trình xử lý nên được kế thừa BaseHandler và thoả mãn can_handle? và handle. Tạo vài trình xử lý khác:
class StripeHandler < BaseHandler private def handle(transaction) puts "handling the transaction with Stripe payment provider" end def can_handle?(transaction) transaction.amount < 100 && transaction.currency == 'USD' end end
class BraintreeHandler < BaseHandler private def handle(transaction) puts "handling the transaction with Braintree payment provider" end def can_handle?(transaction) transaction.amount >= 100 end end
transaction = Transaction.new(100, 'USD') chain = StripeHandler.new(BraintreeHandler.new) chain.call(transaction) # => handling transaction with Braintree payment provider
Tôi đã tạo ra hai trình xử lý. Nếu giao dịch thoả mãn điều kiện trong phương thức can_handle? thì sẽ thanh toán theo phương thức handle.
Trong ví dụ trên, tôi tạo ra đối tượng của lớp StripeHandler và một đối tượng của lớp BraintreeHandler là trình xử lý kế tiếp trong danh sách.
Sau đó gọi call. Giao dịch không thực hiện được call trong StripeHandler, do đó, nó đã đến BaseHandlervà mã này đã được thực hiện:
def call(transaction) return successor.call(transaction) unless can_handle?(transaction) handle(transaction) end
can_handle?(transaction) trên StripeHandler object trả ra false vì số lượng giao dịch lớn hơn 99. Vì vậy, successor.call(transaction) được gọi và trong trường hợp này BraintreeHandler object xử lý được giao dịch, do đó, phương thức handle(transaction) được thực hiện.
Ví dụ 2
Thêm một ví dụ khác để hiểu hơn về Chain of Responsibility Pattern. Giờ tôi có một cửa hàng trực tuyến và tôi cần tính toán mức chiết khấu cho từng khách hàng, tùy thuộc vào nhiều yếu tố. Ví dụ: Các ngày lễ, khách hàng thân thiết, số đơn hàng trước đó, v.v ... Không phải tất cả các chiết khấu đều được áp dụng, ví dụ: Giảm giá Black Friday sẽ chỉ cho một ngày trong năm, giảm giá cho khách hàng trung thành sẽ có sau 5 lần mua hàng, v.v ... vì vậy cần tạo ra một chuỗi các trình xử lý để tính toán mức chiết khấu cuối cùng cho khách hàng.
Tạo một lớp khách hàng, để đơn giản tôi chỉ quản lý số đơn hàng:
class Customer attr_reader :number_of_orders def initialize(number_of_orders) @number_of_orders = number_of_orders end end
Như trong ví dụ trước, tạo lớp BaseDiscount để tính mức chiết khấu:
class BaseDiscount attr_reader :successor def initialize(successor = nil) @successor = successor end def call(customer) return successor.call(customer) unless applicable?(customer) discount end end
Sau đó, thêm những điều kiện giảm giá khác:
class BlackFridayDiscount < BaseDiscount private def discount 0.3 end def applicable?(customer) # ... calculate if it's a black Friday today end end
class LoyalCustomerDiscount < BaseDiscount private def discount 0.1 end def applicable?(customer) customer.number_of_orders > 5 end end
class DefaultDiscount < BaseDiscount private def discount 0.05 end def applicable?(customer) true end end
Áp dụng: chain = BlackFridayDiscount.new(LoyalCustomerDiscount.new(DefaultDiscount.new))
Vì Black Friday là đợt giảm giá lớn nhất nên BlackFridayDiscount sẽ là trình xử lý đầu tiên trong chuỗi. Sau đó đến khách hàng trung thành và nếu cả hai mức chiết khấu đó đều không được áp dụng, sẽ sử dụng chiết khấu mặc định. Chain of Responsibility phải đi từ các trường hợp cụ thể đến các trường hợp chung.
Giả sử doanh nghiệp muốn bỏ giảm giá cho Black Friday. Chỉ cần loại bỏ BlackFridayDiscount khỏi chuỗi và giờ tôi có một chuỗi gồm hai trình xử lý: chain = LoyalCustomerDiscount.new(DefaultDiscount.new)
Mô hình này còn phù hợp với hệ thống trả lời câu hỏi của khách hàng. Bạn có thể tạo ra một chuỗi các câu trả lời từ cụ thể đến chung chung. Khi câu hỏi đi vào chuỗi đó, hệ thống sẽ tìm ra câu trả lời thích hợp nhất. Có thể là một câu trả lời cụ thể cho một câu hỏi cụ thể, hoặc chỉ cần trả lời chung chung nếu không có câu trả lời tốt hơn.
Tôi hy vọng bạn sẽ áp dụng pattern này cho ứng dụng của mình và nó sẽ giúp cải thiện code của bạn. Xin cảm ơn. Nguồn: rubyblog.pro