Two Designs for SequenceIdGenerator

In my previous, marathon post on id generation, I mentioned that I was generating unique sequence numbers. It was straightforward to write a class to generate them:

 
class SequenceIdExhaustion < RangeError ; end
 
class SequenceIdGenerator
  # has 14 bits allocated to it in the CallNumber
  MAX_SEQUENCE_ID = 2 ** 14 - 1
 
  def initialize
    reset!
  end
 
  def reset!
    @sequence_id = 0
  end
 
  def sequence_id
    raise SequenceIdExhaustion if @sequence_id > MAX_SEQUENCE_ID
    @sequence_id
  end
 
  def consume_sequence_id!
    sid = sequence_id
    increment!
    sid
  end
 
  private
 
  def increment!
    @sequence_id += 1
    sequence_id
  end
end

Simple interface: start at zero, a method to consume an id, a method to reset the generator, and a reader to get the id. All while making sure to raise an appropriate exception when the generator has moved into an invalid state.

@pushcx tweet: I seem to be settling on a ruby style where ! indicates methods with side effects

This object is all about maintaining state, so it’s mostly bang methods. That aside, I didn’t love this implementation.

There’s not really any need for the sequence_id getter because that state of “what sequence id are you at now?” doesn’t and shouldn’t matter to any code. And that temporary variable in consume_sequence_id! is just clunky.

So, second pass:

 
class SequenceIdGenerator
  # has 14 bits allocated to it in the CallNumber
  MAX_SEQUENCE_ID = 2 ** 14 - 1
 
  def initialize
    reset!
  end
 
  def reset!
    @sequence_id = -1
  end
 
  def consume_sequence_id!
    increment!
    @sequence_id
  end
 
  private
 
  def increment!
    @sequence_id += 1
    raise SequenceIdExhaustion if sequence_id > MAX_SEQUENCE_ID
  end
end

This has a smaller public interface, which is generally a win. I’ve cleaned up the that temporary variable in consume_sequence_id! in an odd way: by starting in an invalid state.

No one from the outside can see that sequence_id starts invalid, but this is still not OK. Every method potentially needs to account for this. Alternately I could restore the sequence_id getter and have it raise if it’s accessed before consume_sequence_id! has moved it to a valid state, but having a to protect a class from itself just feels wrong.

Here’s a change to remove that temporary variable nicely:

 
  def reset!
    @sequence_id = 0
  end
 
  def consume_sequence_id!
    @sequence_id.tap { increment! }
  end

This uses the K combinator. It used to be implemented in Rails as returning, then migrated into Ruby 1.9 as tap. It’s usually used with the value passed into the block, but works fine without.

After that last long post, I wanted to share this nice little bit of OO design. This keeps state hidden while not succumbing to the temptation of allowing hidden state to be invalid.

Want more? I'm not as good at forgetting to update @pushcx on Twitter.