Chain of Responsibility Pattern - Ruby

Subscribe to receive new articles. No spam. Quality content.

In this article, I'll cover Chain of Responsibility pattern. We will learn how to implement it using Ruby and discover when this pattern is applicable in Ruby apps.

The intent of Chain of Responsibility pattern is to decouple the sender of a request from its receiver by giving more than one object a chance to handle the request.

Don't worry If this definition is too formal, we will consider couple examples that will help you to understand this idea.

Example #1

Let's say we're working on the application that accepts money from customers. Depending on amount and currency we want to use different payment providers to process that money.

To define which payment provider to use for each specific transaction we should run some conditional logic. The code might look like this:

if ...some logic for transaction
  use payment provider 1
elsif ...logic
  use payment provider 2
elsif ...logic
  use payment provider 3
end

If logic is complex, such code can be very cumbersome and hard to refactor.

Chain of Responsibility allows us to build a chain of handlers. Each handler will contain logic to define if the handler can process a transaction.

A transaction will go through that chain until one of the handlers will process it. We can visualize it like this:

Ruby - Chain Of Responsibility

Each handler should contain logic that can decide if this handler is applicable to the transaction, if not it will run the next handler in the chain.

Having this chain, Handler #1 will try to process transaction first. If it can not process that transaction, it will run Handler #2. If Handler #2 can not process a transaction, it will run Handler #3.

Benefits of this approach:

  • we can define an order of handlers
  • each handler contains own conditional logic
  • it's easy to add new handlers
  • we can go from specific handlers to general ones

Let's implement Chain of Responsibility for this example.

First of all, we should create a simple class for a transaction:

class Transaction
  attr_reader :amount, :currency

  def initialize(amount, currency)
    @amount = amount
    @currency = currency
  end
end

Next step would be identifying the interface of our handlers. It would be good if our handlers respond to can_handle? and handle methods. If it can not handle a transaction, it should call the next handler. We will call next handler in a chain successor. I decided to extract this logic into base handler class. Each handler will be inherited from it:

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

I know it's a quite big chunk of code. But let's go line by line to understand how it works.

  def initialize(successor = nil)
    @successor = successor
  end

We accept successor during initialization, so we can create a chain. For example, it could be used like this:

chain = StripeHandler.new(BraintreeHandler.new)
chain.call(transaction)

Since we call call(transaction) method, it's implemented this way:

  def call(transaction)
    return successor.call(transaction) unless can_handle?(transaction)

    handle(transaction)
  end

When we call call(transaction) on the first handler, we check if it can handle this transaction, if it can not, we call successor.call(transaction) and pass flow to the next handler in the chain.

Now we know that each handler should be inherited from BaseHandler and respond to can_handle? and handle messages. Let's implement couple handlers.

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

We created two handlers. The logic that decides if handler should process this transaction lives in can_handle? method. Payment processing lives in handle method.

In the example above, we created an object of StripeHandler with an object of class BraintreeHandler as a successor (the next handler in the list).

Then we called call. There is no implementation of call in StripeHandler, so it went to BaseHandler and this code was executed:

  def call(transaction)
    return successor.call(transaction) unless can_handle?(transaction)

    handle(transaction)
  end

When we run can_handle?(transaction) on the object of class StripeHandler it responded with false because the amount of transaction is more than 99.

So successor.call(transaction) was executed and in our case successor is an object of BraintreeHandler class. It can handle this transaction, so handle(transaction) method was executed.

Example #2

Let's consider another example that should help us to understand the idea of Chain of Responsibility pattern.

We have an online store and we need to calculate a personal discount for a customer, depending on many many factors. For example: Holidays, customer's loyalty, number of previous orders, etc.

It's a great opportunity to create a chain of handlers that would calculate the final discount for the customer. Not all discounts are applicable, for example, Black Friday discount will be available only one day a year, discount for loyal customers will be available after five purchases, etc.

Let's implement it. For a customer, I'll create a really simple class just to show the idea:

class Customer
  attr_reader :number_of_orders

  def initialize(number_of_orders)
    @number_of_orders = number_of_orders
  end
end

To keep things simple, we will track just a number of orders for a customer.

Also, as in the previous example we can create BaseDiscount class and inherit other discounts from it:

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

Then we can add as many discounts as we need:

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

Now we can use them from really specific one to the more general:

chain = BlackFridayDiscount.new(LoyalCustomerDiscount.new(DefaultDiscount.new))

Because Black Friday is the biggest discount customer can get, we will start from it. Then we will try to apply a discount for a loyal customer, and if previous two weren't applied, we will use default discount. Chain of Responsibility should be implemented this way: from specific cases to general ones.

Let's say that business wants to remove discounts for Black Fridays. No problem, all we need to do just remove it from the chain:

chain = LoyalCustomerDiscount.new(DefaultDiscount.new)

Now we have a chain of two. That was easy, right?

This pattern is a good fit for a system where an application should provide an answer to customer's question. You can create a chain of answers from specific to general ones. When question goes on that chain, the system will find the most applicable answer. It can be a specific answer to a specific question, or just general answer if there is no better answer.

Thanks for reading. I hope you'll find how to apply this pattern to your app and it will help to improve your code.

PS: I visited RubyConf in New Orleans and had a chance to say thanks talk to many great people there. Just wanted to admit here that Ruby community is so nice and I'm really thankful to all people who keep working on Ruby language, gems, tools, etc. As Matz said to Ruby developers: "be nice" :)

Subscribe to receive new articles. No spam. Quality content.

Comments