In response to Why metaprogram when you can program?, an astute reader asked for an example of when you would want to use Class.new
in Ruby. It’s a rarely needed method, but really fun when faced with a tasteful application. Herein, a couple ways I’ve used it and an example from the wild.
Dead-simple doubles
In my opinion, the most “wholly legitimate” frequent application of Class.new
is in test code. It’s a great tool for creating test doubles, fakes, stubs, and mocks without the weight of pulling in a framework. To wit:
TinyFake = Class.new do
def slow_operation
"SO FAST"
end
def critical_operation
@critical = true
end
def critical_called?
@critical
end
end
tiny_fake = TinyFake.new
tiny_fake.slow_operation
tiny_fake.critical_operation
tiny_fake.critical_called? == true
TinyFake
functions as a fake and as a mock. We can call a dummy implementation of slow_operation
without worrying about the snappiness of our suite. We can verify that a method was called in the verification section of our test method. Normally you would only do one of these things at a time, but this shows how easy it is to roll your own doubles, fakes, stubs, or mocks.
The thing I like about this approach over defining classes inside a test file or class is that it’s all scoped inside the method. We can assign the class to a local and keep the context for each test method small. This approach is also really great for testing mixins and parent classes; define a new class, add the desired functionality, and test to suit.
DSL internals
Rack and Resque are two examples of libraries that expose an API based largely on writing a class with a specific entry point. Rack middlewares are objects with a call
method that generates a response based on an environment hash and any other middlewares that are contained within the middleware. Resque expects the classes that work through enqueued jobs define a perform
method.
In practice, putting these methods in a class is the way to go. But, hypothetically, we are way too lazy to type class
/end
, or perhaps we want to wrap a bunch of standard instrumentation and logging around a simple chunk of code. In that case, we can write ourself a little shortcut:
module TinyDSL
def self.performer(&block)
c = Class.new
c.class_eval { define_method(:perform, block) }
c
end
end
Thingy = TinyDSL.performer { |*args| p args }
Thingy.new.perform("one", 2, :three)
This little DSL gives us a shortcut for defining classes that implement whatever contract is expected of performer
objects. From this humble beginning, we could mix in modules to add functionality around the performer
, or we could pass a parent class to Class.new
to make the generated class inherit from another class.
That leads us to the sort-of shortcoming of this particular application of Class.new
: if the unique function of performer
is to wrap a class around a method (for instance, as part of an API exported by another library), why not just subclass or mixin that functionality in the client application? This is the question you have to ask yourself when using Class.new
in this way and decide if the metaprogramming is pulling its weight.
How Class.new is used in Sinatra
Sinatra is a little language for writing web applications. The language specifies how HTTP requests are mapped to blocks of Ruby. Originally, you wrote your Sinatra applications like so:
get '/' { [200, {"Content-Type" => "text/plain"}, "Hello, world!"] }
Right before Sinatra 1.0, the team added a cleaner way to to build and compose applications as Ruby classes. It looks the same, except it happens inside the scope of a class instead of the global scope:
class SomeApp < Sinatra::Base
get '/' { [200, {"Content-Type" => "text/plain"}, "Hello, world!"] }
end
It turns out that the former is implemented in terms of the latter. When you use the old, global-level DSL, it creates a new class via Class.new(Sinatra::Base)
and then class_eval
s a block into it to define the routes. Short, clever, effective: the best sort of Class.new
.
So that’s how you might see Class.new
used in the wild. As with any metaprogramming or construct labeled “Advanced (!)”, the main thing to keep in mind, when you use it or when you set upon refactoring an existing usage, is whether it is pulling its conceptual weight. If there’s a simpler way to use it, do that instead.
But sometimes a nail is, in fact, a nail.