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