12/08/2018, 16:32

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

0