viphat / til

Today I Learned
http://notes.viphat.work
0 stars 1 forks source link

[Design Patterns in Ruby] - Chain of Responsibility #233

Open viphat opened 6 years ago

viphat commented 6 years ago

The intent of the Chain of Responsibility pattern is to:

“Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.”

A Chain of Responsibility is used to decouple senders and receivers by giving multiple objects a chance to handle a request. When using a chain of responsibility, each object maintains a pointer to the successor object in the chain.

The ultimate receiver of the request is referred to as an implicit receiver, as the receiver is not known when the request is sent.. It is not explicitly defined. Each object in the chain of responsibility conforms to a shared interface, called the Handler, which defines an interface for handling requests.

# The QuestionHandler class acts as the
# 'Handler' for this implementation of
# the Chain of Responsibility pattern.
class QuestionHandler
  attr_reader :successor

  # We create a pointer to the
  # successor in the chain of
  # responsibility upon intialization.
  def initialize(successor = nil)
    @successor = successor
  end

  def process_request(request)
    # First, the handler attempts to process
    # the request.
    if accept_request(request)
      return

    elsif @successor
      # If the request does not meet the criteria
      # of the handler in question, then, if there
      # is a successor, the request is passed to
      # the successor.
      @successor.process_request(request)
    else
      # If the request does not meet the criteria
      # of this handler, and there is no successor
      # in the chain of responsibility, we'll let
      # the client know that the request has not
      # been fulfilled.
      fail_request(request)
    end
  end

  def fail_request(request)
    puts "The question '#{request}' could not be answered."
  end

  # because we want the 'accept_request' method to be
  # implemented by subclasses of the handler base class,
  # we opt to raise an error if it is not implemented.
  #
  # By the way, the 'accept_request' method, as used in
  # the 'process_request' method, is actually an
  # example of the Template Method pattern.
  def accept_request(request)
    raise '#accept_request method must be implemented.'
  end
end

# The 'StarWarsQuestionHandler' is a concrete
# handler class. It is designed to handle a
# request should that request fulfill certain
# criteria.
class StarWarsQuestionHandler < QuestionHandler
  def accept_request(request)
    # If the request contains the phrase "Star Wars"
    # then this handler will answer the question at
    # hand, then return 'true' to terminate the chain
    # of responsibility. Otherwise, it will forward
    # the question on to other handlers by returning
    # 'false'.
    if request.include?("Star Wars")
      answer_question(request)
      return true
    else
      return false
    end
  end

  # Pretend this does something useful.
  def answer_question(request)
    puts "answering a Star Wars related question: '#{request}'"
  end
end

# The 'HarryPotterQuestionHandler' is a concrete
# handler class. It is designed to handle a request
# should that request fulfill certain criteria.
class HarryPotterQuestionHandler < QuestionHandler
  def accept_request(request)
    # If the request contains the phrase "Harry Potter"
    # then this handler will answer the question at
    # hand, then return 'true' to terminate the chain
    # of responsibility. Otherwise, it will forward
    # the question on to other handlers by returning
    # 'false'.
    if request.include?("Harry Potter")
      answer_question(request)
      return true
    else
      return false
    end
  end

  # Pretend this does something useful.
  def answer_question(request)
    puts "answering a Harry Potter related question: '#{request}'"
  end
end

# Ditto the above two classes.
class LordOfTheRingsQuestionHandler < QuestionHandler
  # BTW, it goes without saying that these question
  # handlers are quite useless, as they only match
  # a single phrase, and provide no actual answers.
  # Therefore, a little imagination must be used
  # on behalf of the reader.
  def accept_request(request)
    if request.include?("Lord of the Rings")
      answer_question(request)
      return true
    else
      return false
    end
  end

  # Pretend this does something useful.
  def answer_question(request)
    puts "answering a Lord of the Rings related question: '#{request}'"
  end
end

# Here, we implement the chain of responsibility.
chain_of_responsibility = HarryPotterQuestionHandler.new(
  StarWarsQuestionHandler.new(
    LordOfTheRingsQuestionHandler.new
  )
)

# We ask questions and we get answers. We have no idea
# which class has answered them, and we don't care. That's
# the beauty of the Chain of Responsibility pattern.
chain_of_responsibility.process_request(
  "What is the longest wand in Harry Potter?"
)
# > answering a Harry Potter related question:
# > 'What is the longest wand in Harry Potter?'

chain_of_responsibility.process_request(
  "How many Jedi have been featured in Star Wars?"
)
# > answering a Star Wars related question:
# > 'How many Jedi have been featured in Star Wars?'

chain_of_responsibility.process_request(
  "Is Lord of the Rings based on a true story?"
)
# > answering a Lord of the Rings related question:
# > 'Is Lord of the Rings based on a true story?'

# The final question had no cooresponding handler, and could
# not be handled. Therefore, we received a failure message.
chain_of_responsibility.process_request(
  "Can anyone name all of the Avengers from the comic books?"
)
# > The question 'Can anyone name all of the Avengers from the
# > comic books?' could not be answered.

Delivery is not guaranteed

One potential downside of using a chain of responsibility is that, because the receiver of the request is not known (implicit receiver), it is possible that nothing fulfills the request. With a chain of responsibility, delivery of a request is not guaranteed.

viphat commented 6 years ago
class PurchaseApprover
  # Implements the chain of responsibility pattern. Does not know anything
  # about the approval process, merely whether the current handler can approve
  # the request, or must pass it to a successor.
  attr_reader :successor

  def initialize successor
    @successor = successor
  end

  def process_request request
    if approve_request request
      return
    elsif successor
      successor.process_request request
    else
      deny_request request
    end
  end

  # This may be overridden by a handler if it wants to provide a custom action
  # when it is the last member of the chain
  def deny_request request
    puts "Your request for $#{request.amount} needs a board meeting!"
  end
end

class AmountApprover < PurchaseApprover
  # Base class for approvers who only consider whether the request amount is
  # allowable
  BASE = 500

  def approve_request request
    if request.amount < self.class::ALLOWABLE
      print_approval request
      return true
    else
      return false
    end
  end
end

class Manager < AmountApprover
  ALLOWABLE = 10 * BASE

  def print_approval request
    puts "Manager will approve $#{request.amount}"
  end
end

class Director < AmountApprover
  ALLOWABLE = 20 * BASE

  def print_approval request
    puts "Director will approve $#{request.amount}"
  end
end

class VicePresident < AmountApprover
  ALLOWABLE = 40 * BASE

  def print_approval request
    puts "VicePresident will approve $#{request.amount}"
  end
end

class President < AmountApprover
  ALLOWABLE = 60 * BASE

  def print_approval request
    puts "President will approve $#{request.amount}"
  end
end

class CFO < PurchaseApprover
  # An example of a handler that does not inherit from AmountApprover
  #
  # Try adding it to the chain by implementing an annual budget that tracks the
  # total of all approved requests so far and whether we can afford one more.
  def approve_request request
    if within_annual_budget? request
      puts "CFO will approve $#{request.amount}"
      return true
    else
      return false
    end
  end

  def within_annual_budget? request
    # ...
  end
end

class PurchaseRequest
  attr_reader :amount

  def initialize(number, amount, purpose)
    @number = number
    @amount = amount
    @purpose = purpose
  end
end

class CLP
  def initialize(*approvers)
    approvers = build_approvers(approvers)
    @authority = approvers.first
  end

  def process_request request
    @authority.process_request request
  end

  private

  def build_approvers(approver_classes)
    [].tap do |approvers|
      approver_classes.reverse.inject(nil) do |successor, approver|
        approver.new(successor).tap {|approver| approvers.unshift approver }
      end
    end
  end
end

# main program
# ------------

approvers = CLP.new(Manager, Director, VicePresident, President)

loop do
  $stdout.flush
  puts
  puts "Enter the amount to check who should approve your expenditure."
  print ">> "
  amount = gets.to_i
  approvers.process_request PurchaseRequest.new(0, amount, 'General')
end