When Mike Burns outlined his vision of Unobtrusive Ruby, I initially thought it was going to be a hit with the community. However, a lack of specific examples led to a backlash of criticism and caused the post to generate more heat than light. This is unfortunate, because the ideas he outlined are quite valuable and shouldn’t be overlooked.

In this article I share my own interpretation of what Unobtrusive Ruby means, based on the the points that Mike outlined. I can’t guarantee that my take on this is what Mike had in mind, but it should be interesting to those who wanted a more well defined roadmap than what he provided. To get this most out of this article, I recommend going back and reading what Mike wrote before continuing on. Try to think about what these concepts mean to you, and then compare them to what I’ve outlined here.

The following guidelines are the ones Mike laid out, but I’ve replaced his explanations with my own in the hopes that attacking these ideas from a second angle will be useful.

Take Objects, Not Classes

Because Ruby lets us pass classes around like any other object, we have a way to do dependency injection that isn’t as common in other languages. For example, we can write something like the following code:

class Roster
 def initialize          
    @participants = []  
  end  
  
  def <<(new_participant)  
    @participants << new_participant  
  end  
  
  def participant_names  
    @participants.map { |e| e.full_name }  
  end  
  
  def print(printer=RosterPrinter)
    printer.new(participant_names).print
  end    
end

class RosterPrinter  
  def initialize(participant_names)  
    @participant_names = participant_names  
  end  
  
  def print
    puts "Participants:\n" +  
    @participant_names.map { |e| "* #{e}" }.join("\n")  
  end  
end 

This feels clever, but this form of dependency injection is more brittle than it needs to be. A recent conversation with Derick Bailey on my SOLID article about a very similar example made me realize just how much we tend to pass classes around unnecessarily when we could be directly passing the objects our class depends on. With that in mind, Derick helped me refactor the previous example to the more flexible design shown below.

class Roster
 def initialize          
    @participants = []  
  end  
  
  def <<(new_participant)  
    @participants << new_participant  
  end  
  
  def participant_names  
    @participants.map { |e| e.full_name }  
  end  
  
  def print(printer=RosterPrinter.new)
    printer.print(@participants)
  end    
end

class RosterPrinter    
  def print(participant_names)
    puts "Participants:\n" +  
    participant_names.map { |e| "* #{e}" }.join("\n")  
  end  
end 

While this is a subtle change, it has a major impact on the way Roster and its printer object relate to one another. The Roster#print method originally had a dependency on both the constructor of its printer object as well as its print() instance method. Our new code reduces that coupling by depending only on the existence of a print() method in the general case. The examples below demonstrate the added flexibility that this new approach offers us.

# does not provide a .new() method
module FunctionalPrinter
  def self.print(participant_names)
    puts "Participants:\n" +  
    participant_names.map { |e| "* #{e}" }.join("\n")  
  end
end

require "prawn"

# has a different constructor than RosterPrinter
class PDFPrinter
  def initialize(filename)
    @document = Prawn::Document.new
    @filename = filename
  end

  def print(participant_names)
    @document.text("Participants", :size => 16)

    participant_names.each do |e|
      @document.text("- #{e}")
    end

    @document.render_file(@filename)
  end
end

roster = Roster.new
roster << "Gregory Brown" << "Jia Wu" << "Jordan Byron"

puts "USING DEFAULT"
roster.print

puts "USING FUNCTIONAL PRINTER"
roster.print(FunctionalPrinter)

puts "USING PDF PRINTER (see roster.pdf)"
roster.print(PDFPrinter.new("roster.pdf"))

Both the FunctionalPrinter and PDFPrinter demonstrate corner cases that our original code did not account for. By following the guideline of accepting objects rather than classes as the arguments to our methods, our new design can accomodate these types of objects without modification of the Roster#print code. As we see from these examples, this makes life easier for us.

Never Require Inheritance

Some libaries we use strongly encourages us to create subclasses of the objects they provide, while others downright force us to do so. ActiveRecord is an example of a library that is almost useless without inheritance, but is very complicated and would be hard to use as an example. The code below is a much simpler example of a tool which expects you to use subclasses of its provided Plugin class to get the job done.

module Inspector
  def self.analyze(data)
    Plugin.registered_plugins.each { |e| e.new.analyze(data) }
  end

  class Plugin
    def self.inherited(base)
      registered_plugins << base
    end

    def self.registered_plugins
      @registered_plugins ||= []
    end

    def analyze(data)
      raise NotImplementedError
    end
  end
end

class WordCountPlugin < Inspector::Plugin
  def analyze(data)
    word_count = data.split(/ /).length
    puts "Content contained #{word_count} words"
  end
end

class WordLengthPlugin < Inspector::Plugin
  def analyze(data)
    longest = data.split(/ /).map { |e| e.length }.max
    puts "Longest word contained #{longest} characters"
  end
end

Inspector.analyze("This is a test of the watcher plugins")

## OUTPUTS
Content contained 8 words
Longest word contained 7 characters   

Using the inherited hook is a clever way to implictly register plugins, but comes with a number of downsides. For example, you are forced to make all your plugins inherit from Inspector::Plugin, which means your class can’t be a subclass of anything else. Additionally, you need to use a class even if a module would make more sense. This is a direct consequence of the “interface taking classes rather than ordinary objects” issue, and cannot be easily avoided. If you throw in things like having to be aware of possible Liskov Substitution Principle violations, it becomes clear that an API which forces you to use subclassing is not exactly flexible.

The code below shows a much more flexible alternative, which completely removes the dependency on class inheritance.

module Inspector
  def self.analyze(data)
    registered_plugins.each { |e| e.analyze(data) }
  end

  def self.registered_plugins
    @registered_plugins ||= []
  end
end

module WordCountPlugin
  def self.analyze(data)
    word_count = data.split(/ /).length
    puts "Content contained #{word_count} words"
  end
end

module WordLengthPlugin
  def self.analyze(data)
    longest = data.split(/ /).map { |e| e.length }.max
    puts "Longest word contained #{longest} characters"
  end
end

Inspector.registered_plugins << WordCountPlugin << WordLengthPlugin
Inspector.analyze("This is a test of the watcher plugins")

Now any object that has a valid analyze method will work as a plugin, as long as you explicitly register it with Inspector. This results in much cleaner looking code for this trivial implementation, but the general strategy can also can be used in situations where inheritance is still a part of the picture. The code below shows plugins which inherit from a parent class coexisting with plugins built from scratch.

module Inspector
  def self.analyze(data)
    registered_plugins.each { |e| e.analyze(data) }
  end

  def self.registered_plugins
    @registered_plugins ||= []
  end

  class Plugin
    def self.verify(&block)
      validations << block
    end

    def self.validations
      @validations ||= []
    end

    def self.analyze(&block)
      define_method :analyze do |data|
        validate(data) 
        block.call(data)
      end
    end

    def validate(data)
      raise unless self.class.validations.all? { |v| v.call(data) }
    end
  end
end

class WordCountPlugin < Inspector::Plugin
  verify { |data| data.is_a?(String) }

  analyze do |data|
    word_count = data.split(/ /).length
    puts "Content contained #{word_count} words"
  end
end

module WordLengthPlugin
  # same as before, not inheriting from anything
end

Inspector.registered_plugins << WordCountPlugin.new << WordLengthPlugin
Inspector.analyze("This is a test of the watcher plugins")

This small change makes a big difference in flexibility, and leaves some of the decision making up to your users rather than forcing them down a particular path. Using the default approach of inheriting from Inspector::Plugin will feel nearly identical to an inheritance-only approach to the user. However, if more customizations are needed, this design provides an easy way to build plugins from scratch. Because it is so easy to implement this pattern, it is probably worth doing even if your immediate needs don’t call for such flexibility.

Inject Dependencies

I covered dependency injection and the dependency inversion principle at length in Practicing Ruby 1.23. Rather than repeating those details here, I’d like you to read that article as well as the comments from Derick Bailey. My conversation with Derick taught me a thing or two about the subtle distinctions between dependency injection (a technique) and dependency inversion (a design principle) that are hard to summarize but still well worth working through if you want to solidify your understanding of these two very important concepts.

As an example of some practical code which uses dependency injection, we can look at the Highline command line library. In ordinary usage, Highline outputs everything to STDIN and STDOUT. You can install HighLine via RubyGems and run the example below to get a feel for how it works.

require "highline"

console = HighLine.new
name    = console.ask("What is your name?") 
console.say("Hello #{name}")

In ordinary cases, this is exactly what we want: ordinary input and output from your console. However, to test HighLine, we made it possible to inject input and output objects to use in place of STDIN and STDOUT. The example below shows the use of StringIO objects, which is what we use in our unit tests.

require "stringio"
require "highline"

input   = StringIO.new("Gregory\n")
input.rewind # set seek pos back to 0

output  = StringIO.new

console = HighLine.new(input, output)
name = console.ask("What is your name?")
console.say("Hello #{name}!")

output.rewind
puts output.read

Interestingly enough, this ‘feature’ of HighLine has caused it to be used in a number of contexts that we didn’t anticipate. For example, it is occasionally used in GUI programs for its input validation features, and sometimes used in non-interactive scripts for its text formatting features. If we had directly worked against STDIN and STDOUT, these ways of using HighLine would not be possible without ugly hacks.

Unit Tests Are Fast

Personally, I am not obsessive about unit test performance. Many Rubyists care a lot about this, and advocate the heavy use of mock objects to speed up your tests. Mike points out in his article that the combination of “mock objects, a lack of inheritance, and injected dependencies” will make your tests fast, and that’s basically true.

Dependency injection does facilitate the use of mock objects, and our HighLine example demonstrates why that is the case. A lack of inheritance might imply that your method call chains are shorter and that you have fewer dependencies, but it’s not as strong of a metric as he makes it seem. Eventually, object composition can end up being more-or-less the same as inheritance in complexity, and in those cases you may still end up with slow tests. But the fact that composed object systems are easier to decouple is probably what he’s getting at here.

For some practical examples of how you can use mocks to decouple your tests from your code and speed things up a bit, see Practicing Ruby 1.20. If you read through that article, you’ll find out why I don’t care so much about limiting my dependencies to only the object under test. But when all else is considered equal, fewer dependencies means less code, which probably means faster tests.

Having a performant test suite is certainly ideal, it’s just a matter of weighing out the costs of fine tuning your test suite vs. the benefits that faster test runs provide. Personally I felt that Mike came on a bit too strong about this particular point, but many well known Rubyists would disagree with me on this one.

Require No Interface

Mike suggests that your library code should allow your user to be able to name their methods however they want, and should be designed to consume rather than be consumed. He then recognizes that this may not always be possible, and concedes that if you need the user to implement certain features, that you should limit it to one or two methods at the most. This is great general advice, and if you look at Ruby itself, we can find some good examples.

The Enumerable module is capable of providing the vast majority of its features if the user implements only an each method. If you want to use things like sort, you only need the yielded elements to implement a <=> method. This means that all features in Enumerable can be supported by “one or two methods” implemented by the user, which makes it a very good API considering the great wealth of functionality it provides.

However, Enumerator takes things a step farther by requiring no interface at all. You can name the methods of the target collection anything you want, you simply need to tell Enumerator which method it should delegate its each method to when you initialize an Enumerator. See the example below to see how flexible this approach is.

class RandomInfiniteList
  def generate
    loop do
      yield rand
    end
  end
end

enum = Enumerator.new(RandomInfiniteList.new, :generate)
p enum.next
p enum.take(10)

In this way, Enumerator can be made to turn any object with an iterator method into an Enumerable object, regardless what the name of that method is. This can be useful when you’re working with unidiomatic Ruby objects which provide an iterator but do not mix in Enumerable.

We can also look at a bit less abstract of an example. If we look back at our Inspector example that we were using to discuss how to avoid hard inheritance requirements, we can see that it requires only a small interface for plugins to conform to. While it isn’t so bad that each plugin needed an analyze method in our last iteration, we can make some modifications to make it so that it depends on no interface at all, which may bring us a step closer to what Mike was hinting at.

The example below shows what an Inspector class which implements a “expects no interface” API style might look like. To keep things interesting, I’ve left implementing this class up to you. If you get stuck, feel free to leave a comment asking for hints on how to build this out.

# delegates to the WordLengthPlugin module
Inspector.report("Word Length") do |data|
  WordLengthPlugin.analyze(data)
end

# implements the report as an inline function
Inspector.report("Word Count") do |data|
  word_count = data.split(/ /).length
  puts "Content contained #{word_count} words"
end

Inspector.analyze("This is a test of the watcher plugins")

Data, Not Action

This guideline seems to boil down to the idea that your API calls should be simple ‘data in, data out’ operations whenever that level of simplicity is easily within reach. After thinking about this concept a bit, I realized that a pair of common Ruby operations serve as perfect examples.

Suppose that you want to open a binary file and read its contents into a String. You could write the following code and it will get the job done.

file = File.open("foo.jpg", "rb")
image_data = file.read
file.close

But this approach is only the correct way to do things if you want to explicitly work with the File object and control exactly when it gets opened and closed. If all you want to do is read the contents of a binary file, this is three actions for what should be one “data in, data out” operation. Fortunately, Ruby recognizes this as a common use case and provides a nicer API that you can use instead.

image_data = File.binread("foo.jpg")

In this example, we pass the filename and get back the contents of that file data. While we have less control over the overall process, we also get to ignore irrelevant details like exactly when the file is opened and closed. This is what I think that Mike probably meant when he said “data, not action”, and can be applied when designing your own APIs. To drive the point home further, we can look at one example.

The code below shows the generic form of doing a GET request via the Net::HTTP standard library. It is not the most terse way to use Net::HTTP, but one of the most common.

require 'net/http'

url = URI.parse('http://www.google.com')
res = Net::HTTP.start(url.host, url.port) do |http|
  http.get('/')
end

puts res.body

There is a whole lot going on here, including explicit URI parsing and explicit calls to get(). But as we saw with File.open vs. File.binread, Ruby provides a convenient alternative for this very common operation. The open-uri standard library makes it possible to write the following code instead.

require "open-uri"

puts open("http://google.com").read

Once again, we see a series of complex actions being converted into a simple “data in, data out” operation. In this case, we are converting a string which represents a URI into an IO object via the globally available open() method. This makes it possible for us to not think about explicitly parsing our string into a URI object, and lets us ignore the details about starting a HTTP connection and explicitly making a GET request. When all we want is the source of a web page, it’s great to be able to ignore these details.

Always Be Coding

As I reflect back on Mike’s guidelines for “Unobtrusive Ruby”, it all seems to come down to making life easier for your users by limited the amount of impact your system has on them. Give your users small, flexible APIs that do not demand much of their systems, and they will have better luck using your code. This seems like a noble set of goals to me, and hopefully my examples demonstrate the same spirit that Mike wanted to encourage in his post.

However, I always worry about using design guidelines like these as some sort of absolute set of commandments. There were a whole lot of words like “always” and “never” in the original “Unobtrusive Ruby” post which if left unchecked, could cause more harm than good. For me, context is king, and these ideas seem much more important for those who are writing code that is intended for widespread reuse than they might be for ordinary application development. That having been said, the examples I’ve shown here demonstrate that you can often be on the right side of these guidelines with little to no additional effort. This means that if we can keep the ideas behind “Unobtrusive Ruby” in the back of our mind and apply them when it’s an easy fit, we may well end up improving our code for the better.

This article is meant to be a conversation starter, and is not meant to be taken as gospel. Please share your own thoughts on what “Unobtrusive Ruby” means to you, either via a comment here or on your own blog. I think it’s a conversation worth having, even if we haven’t quite nailed down all the definitions yet. If we end up getting an interesting discussion going, I’ll invite Mike to check out what we’ve discussed and see what he thinks about it.

So what do you think? Was my code unobtrusive enough for you? If not, why not? ;)