This technique came from my work on table_print, an irb tool for showing ruby objects in tables. I wanted to let people configure its output on a process-wide basis; eg, a default date format so sll their dates are shown consistently in the way they prefer. Additionally, I wanted a simple, easy-to-remember interface and a clean global namespace.
Here's a basic syntax example for table_printing:
# prints all the authors, along with their book titles and photo captions > tp Author.all, :include => "books.title", "books.photos.caption" NAME | BOOKS.TITLE | BOOKS.PHOTOS.CAPTION ------------------------------------------------------------------------- Michael Connelly | The Fifth Witness | Susan was running, fast, away... | | Along came a spider. => 0.58217
The tp method is the only interface to the application; all input is passed to that method from the command line. A great config interface would be something like:
> tp.set :default_date_format, "%m-%d-%Y" => Config Saved. # NOTE: this is equivalent to: > tp().set(:default_date_format, "%m-%d-&Y") # and technically, there's no reason we couldn't print AND set config: > tp(Author.all, :include => "books.title", "books.photos.caption").set(:default_date_format, "%m-%d-&Y") # ...except we want to be able to remove the parentheses and simply call tp.set - the cleanest possible interface
By reusing the tp method we maintain a consistent interface and add nothing to the global namespace.
The .set call is operating on the return value of the tp method. Unfortunately, the return value is also used when doing a normal table_print. Looking at the first example, the return value is shown on the last line: => 0.58217. It's the time it takes to print the objects. I chose that value because a) it had to be something, and b) it had to be short. Returning the objects themselves would result in a nicely formatted table followed by the block of mush we were trying to avoid in the first place! So paradoxically, we need to both return something short and useful, that also happens to be a config object.
So we create a Returnable object that both passes config methods through to TablePrint::Config, and also defines a to_s method that returns the decimal.
class Returnable def initialize(string_value) @string_value = string_value end def set(*config) TablePrint::Config.set(config) @string_value = "Config Saved." self end def to_s @string_value end end
This dual-use object lets us return one thing from the tp, confident that it will handle either a print or a config.
def tp(data=, *options) time = Time.now # maybe print some stuff, maybe not! depends if there's any data TablePrint::Printer.new(data, options).print # always return something suitable for EITHER display or setting config Returnable.new(Time.now - time) end
Let's follow the code flow for setting config to see how this works in practice.
# call into the tp method with no arguments. The .set will operate on the return value of the tp method! > tp.set :default_date_format, "%m-%d-%Y" # tp method does its thing def tp(data=, *options) time = Time.now # data is an empty array, options is nil, so TablePrint performs a no-op TablePrint::Printer.new(data, options).print # returnable is created and returned to the caller (the console) Returnable.new(Time.now - time) end # console regains control and continues evaluating the input. It calls .set on the object returned by the tp method: returnable.set :default_date_format, "%m-%d-%Y" # returnable.set passes the config through to TablePrint def set(*config) TablePrint::Config.set(config) @string_value = "Config Saved." self end # console regains control, calls .to_s on the object returned by the returnable.set method. => Config Saved.