An attack of over-engineering

I just had a horrible attack of over-engineering. Thankfully, I managed to pull out of it before it went too far. It was like a glimpse into the life of an fully-indoctrinated Java programmer (you know, the ones with hammer-shaped copies of the "Design Patterns" book by the Gang of Four, to whom every software problem looks like a nail).

Ironically, I started a new script precisely because I wanted to replace a piece of over-engineered code (Capistrano) with something simple (a single script that deploys my Rails application).

First step. Write a method for forking an external command, capturing its output and exit status, and returning the result. This was something I've done quite a few times over the years, so I just did a copy-and-paste from the last project where I did it.

Second step. Look at inspect output from returned object (an OpenStruct instance), and decide that you don't like how the word "OpenStruct" is visible in the output. This is where the madness began. I had chosen to use OpenStruct precisely because it provided a nicer-looking interface for the object containing the returned results (nicer than a Hash, I mean) and because it was already written for me. But worrying about what the debugging output looked like when I was certain that once written, the script would never need to be debugged again? I should have perceived the signs of pending insanity.

Third step. Create a custom class to encapsulate the results. Decide, for some unfathomable reason, that you want the attributes to be readable freely, but writable only once. Why? I still don't know why I care about this.

Fourth step. Do some meta-programming (dynamically adding methods via class_eval) to add special accessor methods to your custom class, methods which enforce the desired read/write behaviour. Why meta-programming? To keep things DRY, of course; we're talking about four attributes here! More madness of course as the whole thing shouldn't have even been done once, let alone repeated. (I guess I really needed to DOY, or "Don't Once Yourself!").

Fifth step. Look at your new custom object and decide that you don't like that object's inspect output either, because the internal variables you've set up to make sure that attributes only get written once appear and you'd rather they not. Refactor to use one private variable only (an Array for keeping a list of attributes), and generate your inspect output by introspecting the Array.

I'm ashamed to post this hideous thing, but I will so that others may learn from my stupidity. Here's what the code is looking like by now:

class ExecRecord
  def initialize
    # keep track of attributes for making inspect pretty
    @attributes = []
  end

  # define accessors for an attribute that can be written once, read many times
  def self.define_accessor *args
    args.each do |arg|
      arg = arg.to_s
      class_eval <<-DEF
        # def foo
        #   @foo
        # end
        def #{arg}
          @#{arg}
        end

        # def foo= val
        #   if @attributes.include? 'foo'
        #     raise "attempt to modify write-once attribute, foo"
        #   end
        #   @foo = val
        #   @attributes << 'foo'
        # end
        def #{arg}= val
          if @attributes.include? '#{arg}'
            raise "attempt to modify write-once attribute, #{arg}"
          end
          @#{arg} = val
          @attributes << '#{arg}'
        end
      DEF
    end
  end
  define_accessor :stderr, :stdout, :status, :command_string

  def inspect
    content = @attributes.collect do |attr|
      ivar = instance_variable_get("@#{attr}")
      "@#{attr}=#{ivar.inspect}"
    end
    content.unshift "#{self.class.to_s}:#{'0x%x' % object_id}"
    "\#<#{content.join(' ')}>"
  end
end # class ExecRecord

The original method which uses this abomination looks like this. You can see the OpenStruct creation, where the horrible ExecRecord would be slotted in:

def run cmd, *args
  result = OpenStruct.new
  result.stdout = ''
  result.stderr = ''
  Wopen3.popen3(*([cmd] + args)) do |stdin, stdout, stderr|
    threads = []
    threads << Thread.new(stdout) do |out|
      out.each { |line| result.stdout << line }
    end
    threads << Thread.new(stderr) do |err|
      err.each { |line| result.stderr << line }
    end
    threads.each { |thread| thread.join }
  end
  result.status = $?.exitstatus
  result.command_string = ([cmd] + args).join(' ')
  result
end

Sixth step. By now you're starting to realize that all this is probably misguided but you're almost falling asleep so you continue for long enough to jot down some "TODO" notes for tomorrow. Things like, "send freeze to attribute storage to stop callers from modifying contents" (why the hell was I worried about this?).

Or, "move execution method inside record class to keep everything encapsulated"... So this helper class that I decided to whip up for use in a method has started to become such a protagonist that I'm looking at swallowing up the original method into the helper method. This is surely a sign that this thing has started to grow at a tumour-pace.

Or, "set up top-level run method as convenience for creating new instances"... But wasn't that what I started with? A top-level convenience method for running external commands? I'm going in circles here. Time to stop.

I really shouldn't be playing with code when I'm sleep-deprived like this.

← 0 minutes with Vim
Giving up on Emacs →

All blog posts