Chain of Responsibility Pattern - Ruby
Bài đăng này đã không được cập nhật trong 3 năm
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 BaseHandler
và 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
All rights reserved