Chain of Responsibility Pattern

Updated . Posted . Visible to the public.

The chain of responsibility is most useful when related objects are all trying to handle the same request. Let's say we have a series of image objects (Jpeg, Gif, PNG) that are all trying to import an image. Each will extract and store meta data from the image and maybe create a standard-use thumbnail, but how depends on which kind of image. And so we have if statements to use the Jpeg image to import jpeg images, the Gif image to import Gif images, and so on. The conditionals properly send a file ending with .jpg to JpegImage. A file ending with .gif goes through GifImage. The unknown image doesn’t get picked up by anything so it gets handled byUnknownImage (likely storing the raw date without meta info or thumbnail). But the .jpeg image and the JPEG image with the wrong extension are both mishandled.

if extension == 'gif'
  GifImage.new(filename).import
elsif (extension == 'jpg')
  JpegImage.new(filename).import
elsif (extension == 'png')
  PngImage.new(filename).import
else
  UnknownImage.new(filename).import
end

The nifty idea of the chain of responsibility comes from the recognition that the different handlers (JpegImage, GifImage, PngImage, etc.) are a… chain. A chain of request handlers. If one link in the chain cannot handle the request — if one image handler cannot handle the import request — then the next handler in the chain gets a chance. Phrased like that, we can push the requests down into the handler method — import(). If the current handler cannot handle the request, then ask the next handler to do so. If it cannot, then it asks the next handler to try. And so on until the request is handled.

%w(asdf.gif bar.jpeg baz.png foo.jpg jpeg.aaa 1.txt).each do |filename|
  ImageImporter.new(filename).import
  ImageImporter.new(filename).import_only_when_jpeg
end

### Pattern name: Handler ###
class Image
  def initialize(file_name)
    @file_name = file_name
  end

  ### Pattern name: Handle Request ###
  def import
    @next_importer.import
  end

  def import_only_when_jpeg
    @next_importer.import_only_when_jpeg
  end

  def extension
    parts = @file_name.split('.')

    return nil if parts.length != 2
    parts[1]
  end

  def read_initial_bytes
    FileMock.open(@file_name, 'rb') do |file|
      file.read
    end
  end
end

### Pattern name: Concrete Handler ###
class ImageImporter < Image
  def initialize(file_name)
    super(file_name)
    @next_importer = JpegImage.new(file_name)
  end
end

class JpegImage < Image
  def initialize(file_name)
    super(file_name)
    @next_importer = GifImage.new(file_name)
  end

  def import
    return super unless can_import?
    make_import
  end

  def import_only_when_jpeg
    return super unless can_import?
    make_import
  end

  private

  def make_import
    puts "Importing \"#{@file_name}\" as a JPEG image."
  end

  def can_import?
    extension == 'jpg' || extension == 'jpeg' || read_initial_bytes.include?('JPEG')
  end
end

class GifImage < Image
  def initialize(file_name)
    super(file_name)
    @next_importer = PngImage.new(file_name)
  end

  def import
    return super unless can_import?
    puts "Importing \"#{@file_name}\" as a GIF image."
  end

  private

  def can_import?
    extension == 'gif'
  end
end

class PngImage < Image
  def initialize(file_name)
    super(file_name)
    @next_importer = UnknownImage.new(file_name)
  end

  def import
    return super unless can_import?
    puts "Importing \"#{@file_name}\" as a PNG image."
  end

  private

  def can_import?
    extension == 'png'
  end
end

class UnknownImage < Image
  def initialize(file_name)
    super(file_name)
  end

  def import
    puts "Importing \"#{@file_name}\" as an unknown image."
  end

  def import_only_when_jpeg
    puts "Can't import \"#{@file_name}\" because it ain't JPEG"
  end
end

### Helper code, to mimic ruby standard File object ###
class FileMock
  MOCKS_HASH = {
      'asdf.gif' => 'GIF',
      'bar.jpeg' => 'JPEG',
      'baz.png'  => 'PNG',
      'foo.jpg'  => 'JPEG',
      'jpeg.aaa' => 'JPEG'
  }

  def self.open(path, *args, &block)
    yield FileMock.new(path)
  end

  def initialize(file_path)
    @file_path = file_path
  end

  def read
    return MOCKS_HASH[@file_path] if MOCKS_HASH.has_key?(@file_path)
    'HUH!?'
  end
end
Alexander M
Last edit
Alexander M
Posted by Alexander M to Ruby and RoR knowledge base (2017-02-02 15:38)