Have you killed a design pattern today?

Design patterns are the enemy of agility. They introduce repetition and accidental variation to your codebase. Design patterns encourage you to create "point solutions" throughout your application, instead of cleanly isolating concerns. And they will make your code refactor-proof, no matter how cool your IDE is. But there is hope: Catch your design patterns while they are young, and teach them to be library calls instead. Here's one example:

In Ruby, we often re-open existing classes and add instance methods. One approach is simply to open the class:

  class NilClass
    def blank? 
      true
    end
  end

Or, you could create a new module and mix it in:

  module MyNilExtensions
    def blank?
      true
    end
  end
  class NilClass
    include MyNilExtensions
  end

There are other approaches that are similar but not quite the same. In other words, this is a design pattern. From the Wikipedia entry:

A design pattern is not a finished design that can be transformed directly into code. It is a description or template for how to solve a problem that can be used in many different situations.

The problem with design patterns is the "not a finished design" part of the definition. Rather that a DRY solution, design patterns give you repetition throughout the code. Worse yet, the repetition is not exact. It is repetition with variation, and there is often no evidence whether the variation is intentional or accidental.

I like Ruby because I can eliminate design patterns when they start to annoy me. This "Open Class Add Method" pattern annoyed me for the last time earlier today, when two different libraries defined incompatible versions of Object#metaclass. Enough is enough. Let's make a library call for reopening classes.

Here are my design goals:

  1. The syntax should be terse. I hate defining a module and including it in two separate steps, I just want to open a block and go.
  2. I should have an audit trail for where new methods came from. (This is one reason to define a named model, because I can then reflect against it.)
  3. I should be protected from method collisions. This protection should be configurable. Collisions can be explicitly approved, or they can generate a warning, or an error.

These three goals are in conflict (and we could easily come up with more). This illustrates another problem with design patterns. Each time a design pattern is used, a programmer favors some design goals over others. Over time, this leads to a codebase at odds with itself. If the same pattern were captured in a reusable module, then changing design priorities could be handled from that module alone.

Here's a strawman proposal for cleanly adding methods to existing Ruby classes. The following code adds #jump to Object:

  embrace{Object}.and_extend do
    def jump
      puts "jumping"
    end
  end

The syntax is simple and involves just one block, meeting goal one. Behind the scenes, I use __FILE__ and __LINE__ to define a module, which gives us auditability (goal two):

  > puts Object.ancestors
  Object
  Anonymous module from /Users/stuart/Desktop/temp.rb 56
  Kernel  

Finally, the code that mixes in the module walks the inheritance hierarchy first, printing a warning whenever a name collision is encountered (goal three).

  Warning: /Users/stuart/Desktop/temp.rb 64 is attempting to redefine jump
           Originally defined in /Users/stuart/Desktop/temp.rb 56  

The complete implementation is included at the bottom of this post. I am sure it can be improved in several ways, but even in its primitive state it beats a design pattern. As long as the API is decent, we can always make the implementation suck less later.

Should I make a gem out of this? What changes would you like to see in the API? How should the handling of method collisions be specified?

  require 'set'
  module Embrace
    class <<self
      def check_for_collisions(clazz, module_to_include)
        new_methods = Set.new(module_to_include.instance_methods(false))
        clazz.ancestors.each do |anc|
          anc.instance_methods(false).each do |meth|
            collision(anc, module_to_include, meth) if new_methods.member?(meth)
          end
        end
      end

      def collision(clazz, module_to_include, method)
        puts "Warning: #{module_to_include} is attempting to redefine #{method}"
        puts "         Originally defined in #{clazz}"
      end
    end
  end

  def embrace(&clazz_block)
    m = Module.new
    file = eval("__FILE__", clazz_block.binding)
    line = eval("__LINE__", clazz_block.binding)
    clazz = clazz_block.call
    meta = class << m; self; end
    meta.class_eval do
      def and_extend(&blk)
        self.class_eval(&blk)
        mixin_to_class
        self
      end
      define_method("mixin_to_class") do
        Embrace.check_for_collisions(clazz, m)
        clazz.class_eval do
          include m
        end
      end
      define_method("to_s") do
        "Anonymous module from #{file} #{line}"
      end
    end   
    m 
  end

  o = Object.new

  embrace{Object}.and_extend do
    def jump
      puts "jumping"
    end
  end

  o.jump

  embrace{Object}.and_extend do
    def jump
      puts "jumping higher"
    end
  end

  o.jump