Ruby, iOS, and Other Development

A place to share useful code snippets, ideas, and techniques

All code in posted articles shall be considered public domain unless otherwise noted.
Comments remain the property of their authors.

2006-09-26

Simple Flyweight Implementation

The Flyweight design pattern can be handy for a variety of purposes (which is, of course, why it is a design pattern). I'll give a contrived example of a situation in which it might be desirable, and a simple wrapper to provide flyweight functionality. Imagine a situation in which several string formats are used around your code:

class Foo
  def to_s
    sprintf("%03d:%d", @line_num, @line.split(/\s+/).size)
  end
end

class Bar
  def word_count(line_num)
    sprintf("%03d:%d", line_num, @lines[line_num].split(/\s+/).size)
  end
end

Please remember that this is a contrived example. The point is that similar things are being done in both methods, and it's a good candidate for refactoring. So, instead, consider:

class Formatter
  class << self
    alias :create :new
  end

  def initialize(fmt, word_count_args)
    @fmt = fmt
    @word_count_args = word_count_args
  end

  def format(*args)
    args = args.zip(@word_count_args).map { |val,count|
      count ? val.split(/\s+/).size : val
    }
    sprintf(@fmt, *args)
  end
end

class Foo
  def to_s
    fmt = Formatter.create("%03d:%d", [ false, true ])
    fmt.format(@line_num, @line)
  end
end

class Bar
  def word_count(line_num)
    fmt = Formatter.create("%03d:%d", [ false, true ])
    fmt.format(line_num, @lines[line_num])
  end
end

Now we have a situation in which a new Formatter object is created with every call to either method. Let's bring in the flyweight (classes Foo and Bar do not change):

module Flyweight
  def flyweight_cache(*config)
    (@flyweight_cache ||= {})[config] ||= yield config
  end
end

class Formatter
  extend Flyweight

  def self.create(fmt, word_count_args)
    flyweight_cache(fmt, word_count_args) { |format,wc_args|
      new(format, wc_args)
    }
  end

  def initialize(fmt, word_count_args)
    @fmt = fmt
    @word_count_args = word_count_args
  end

  def format(*args)
    args = args.zip(@word_count_args).map { |val,count|
      count ? val.split(/\s+/).size : val
    }
    sprintf(@fmt, *args)
  end
end

So now only one Formatter object is ever created, and it's retrieved on each method invocation (except the first one). How simple is that? Enjoy!

Labels: ,