Have you killed a design pattern today?

  • Posted By Stuart Halloway on February 13, 2008

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  
Comments
  1. David ChelimskyFebruary 13, 2008 @ 06:25 AM

    How about this:

    reopen Object do def jump puts “how high?” end end

    Seems simpler and the name tells you what it’s doing. Not sure if it would be as easy to implement. WDTY?

  2. David ChelimskyFebruary 13, 2008 @ 06:33 AM

    Here’s a crack at it: http://pastie.caboo.se/151399

  3. coderrrFebruary 13, 2008 @ 09:27 AM

    good idea…

    how about Object.safe_extend { ... } or safe_extend(Object) { ... } ?

  4. Jim WeirichFebruary 13, 2008 @ 02:03 PM

    I’m wondering if the techniques used in BlankSlate to detect method additions could be used by embrace_and_extend to detect traditional class extensions. Then you could warn against anyone redefining jump, not just those who are using embrace_and_extend.

    (The BlankSlate technique is written up here: http://onestepback.org/articles/advanced_ruby_class_design/AdvancedClassDesign.pdf).

  5. Glenn VanderburgFebruary 13, 2008 @ 02:52 PM

    Stu, the key API difference between your strawman and David’s is that you use a block to specify the class to open, and David just passes the parameter. Your use of a block seemed odd to me the minute I looked at it, but I’m assuming you had a reason. Care to elaborate?

  6. StuFebruary 13, 2008 @ 03:03 PM

    David: your approach is definitely an improvement. Glenn: I was being silly, I just liked being able to say “embrace … and extend. Bwa ha ha!” Jim: Capturing tradititional class extensions would be cool, and certainly would provide immediate benefit for existing code. Still, I wonder if opening and re-opening are not different enough that they deserve a different syntax.

    All: so should we build this thing? If there was a reopen capability, would you port existing code to use it, for clarity?

  7. R.J. OsborneFebruary 13, 2008 @ 05:33 PM

    Re-opening is certainly a different case. It would be wonderful to get a nice, fat warning when I step on a toe- think metaprogramming- my test should fail beautifully.

  8. Lucas HĂșngaroFebruary 13, 2008 @ 05:55 PM

    That’s very, very nice.

    +1 for a gem!

  9. jherberFebruary 13, 2008 @ 06:15 PM

    just some food for though – does the argument for lexically scoped open classes hold water in ruby as solution? and if so, what would lexically scoped open classes look like in ruby ?

    inspired by a post from the scala side of the world.

    http://debasishg.blogspot.com/2008/02/why-i-like-scalas-lexically-scoped-open.html

  10. sinclairFebruary 13, 2008 @ 07:11 PM

    Stu,

    As always I am looking for enlightenment …

    How does the approach discussed above offer an alternative to (or a counter to) a design pattern ?

    If you’re tone was intended as humour then please disregard the rest of this.

    Ah you were’nt being funny =(

    Ok. I am clearly missing something then in this discussion.

    To crack open a class or an instance of a class does not (to my mind) define an alternative. The problem(s) still exist the solutions to the problems exist and design patterns offer means of describing recurring problems and a possible solution with trade-offs. At a programmer level they offer a language or vocabulary which can aid in communicating.

    Ruby includes some features which are commonly understood as design patterns. These are useful additions to the language – I think.

    The open-and-live characteristic offered by Ruby is a risky business as you point out. So why do you prefer it as an alternative to design patterns. I just do not see the link.

    Cheers! sinclair

  11. coderrrFebruary 13, 2008 @ 07:20 PM

    here’s a simplified version of your code: http://pastie.caboo.se/151667

  12. JakeFebruary 13, 2008 @ 09:07 PM

    If this would provide full traceability, then man, I’m on-board. I’ve often found myself tracing through code to find out where behavior was added (or broken).

  13. Ryan Carmelo BrionesFebruary 15, 2008 @ 08:47 PM

    Although I think having an audit of reopened classes is awesome, I think this is a poor example of “killing a design pattern”. It seems like the act you are describing is more akin to “cargo-culting” and less of a misuse of design patterns.

  14. jgehtlandFebruary 17, 2008 @ 10:28 PM

    @Sinclair— the design pattern being redressed here is that of opening a class and adding a method. The solution to the pattern is to create a library call that does the repetitive (and risky) work of extending a class gor you, the providing a more direct approach, and a central way to extend the capabilities of the solution. Stu is not suggesting that open classes are themselves an alternative to patterns.

    @Ryan—I thing that stu was describing a “design pattern”, not a “Design Pattern”, meaning it represents a commonlaw pattern that exists through accretion as opposed to having been published. That very well might be the same thing as a cargo cult, but I don’t think that distinction detracts from Stu’s argument. I frankly think this whole post could have been just a link a Paul Graham’s quote about design patterns being simply macros he hasn’t written yet. But the code sample makes a better argument.